swarm-matrix-ctl: one control binary for the matrix container, not one per job
Renames `swarm-matrix-minter` and reshapes it around subcommands. Minting is now `swarm-matrix-ctl mint`. Running rust inside `containers.hive-matrix` is not free: it needs its own store identity, its own cert role and its own bind mounts, and every one of those is per-*container*, not per-task. A second single-purpose crate would have had to duplicate that plumbing to add one action, so the next thing that has to run in there should be a verb here rather than a new crate. The old name guaranteed the opposite. `main.rs` is clap dispatch; the minting logic moves to `mint.rs` unchanged. A bare invocation is refused: `mint` writes a credential, so "no verb" defaulting to it would make a typo in the unit mint rather than fail. The environment prefix moves with it, `MATRIX_MINTER_*` → `MATRIX_MINT_*`. Scoped to the verb and not to the binary, because a binary-scoped prefix is one the next verb has to share or widen, and a widened one never narrows again. A test asserts every variable carries the verb's prefix. The principal renames too. The cert role, bao policy, granting unit, leaf filename and `certAuthCns` entry all have to spell one string the same way, so leaving them as `swarm-matrix-minter` would have rebuilt the naming split this branch exists to remove. Renaming the nix options alongside is free here: every one of them is introduced by this PR and has never been released, so no operator config names them yet. `ExecStart` now names the verb, which is a contract between a nix string and a clap enum that fails at deploy time with no local signal. Both ends assert it: `mint_is_spelled_the_way_the_unit_invokes_it` in the crate, and a new module-eval arm reading the rendered `ExecStart`. docs/getting-started/setup.md drops the sender token from its "live on the host" list: setup does not touch this credential, so a setup guide has no reason to name it.
This commit is contained in:
parent
fb9c6122df
commit
67ba28448f
23 changed files with 319 additions and 172 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -4925,10 +4925,11 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "swarm-matrix-minter"
|
name = "swarm-matrix-ctl"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"clap",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"swarm-secret-client",
|
"swarm-secret-client",
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ members = [
|
||||||
"swarm-authelia-bridge",
|
"swarm-authelia-bridge",
|
||||||
"swarm-authelia-bridge-sock",
|
"swarm-authelia-bridge-sock",
|
||||||
"swarm-controller",
|
"swarm-controller",
|
||||||
"swarm-matrix-minter",
|
"swarm-matrix-ctl",
|
||||||
"swarm-nats-auth",
|
"swarm-nats-auth",
|
||||||
"swarm-queue-client",
|
"swarm-queue-client",
|
||||||
"swarm-logs",
|
"swarm-logs",
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,7 @@ See [`tools/hivectl.md`](../tools/hivectl.md) for every `hivectl` verb.
|
||||||
|
|
||||||
- **No forge admin token is stored in any agent state dir.** Agents
|
- **No forge admin token is stored in any agent state dir.** Agents
|
||||||
hold a regular agent token in their `forge-token` file; sensitive
|
hold a regular agent token in their `forge-token` file; sensitive
|
||||||
creds (the core token, the matrix sender token) live on the host.
|
creds (the core token) live on the host.
|
||||||
- All config changes (forge PRs on `agent-configs/<name>`) go through
|
- All config changes (forge PRs on `agent-configs/<name>`) go through
|
||||||
operator approval — agents can't unilaterally rebuild containers, by design.
|
operator approval — agents can't unilaterally rebuild containers, by design.
|
||||||
See [`boundary.md`](../trust-boundary/boundary.md) and [`security.md`](../trust-boundary/security.md).
|
See [`boundary.md`](../trust-boundary/boundary.md) and [`security.md`](../trust-boundary/security.md).
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ homeserver runs no `admin_execute` for it.
|
||||||
|
|
||||||
Its access token is the **sender token**, and it's the credential
|
Its access token is the **sender token**, and it's the credential
|
||||||
hive-c0re presents for every homeserver call it makes on the hive's
|
hive-c0re presents for every homeserver call it makes on the hive's
|
||||||
behalf. `swarm-matrix-minter` mints it inside the `hive-matrix`
|
behalf. `swarm-matrix-ctl` mints it inside the `hive-matrix`
|
||||||
container and publishes it to `swarm/services/matrix/sender-token`; the
|
container and publishes it to `swarm/services/matrix/sender-token`; the
|
||||||
hive reads it from there. The name says what it authenticates as — an
|
hive reads it from there. The name says what it authenticates as — an
|
||||||
account the appservice registration brings into being — rather than any
|
account the appservice registration brings into being — rather than any
|
||||||
|
|
|
||||||
|
|
@ -54,15 +54,15 @@ strategy for every credential, including the mTLS leaf.
|
||||||
|
|
||||||
<!-- vale write-good.Passive = NO -->
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
| store path | minter | reader — pulls at runtime, holds in memory | renewal |
|
| store path | minter | reader — pulls at runtime, holds in memory | renewal |
|
||||||
| -------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
|
| -------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- |
|
||||||
| `swarm/agents/<agent>/matrix/<account>` | `swarm-controller` | the agent container itself, under the certificate its hive passed in | must be stated |
|
| `swarm/agents/<agent>/matrix/<account>` | `swarm-controller` | the agent container itself, under the certificate its hive passed in | must be stated |
|
||||||
| `swarm/agents/<agent>/bao-mtls` | `swarm-controller`, at agent creation | `hive-c0re`, under the hive's own certificate, when it writes the agent's container config | must be stated |
|
| `swarm/agents/<agent>/bao-mtls` | `swarm-controller`, at agent creation | `hive-c0re`, under the hive's own certificate, when it writes the agent's container config | must be stated |
|
||||||
| `swarm/hives/<hive>/matrix/appservice-token` | one minter, on the authelia host | the hive process that presents the token to its homeserver, under the hive's own certificate | must be stated |
|
| `swarm/hives/<hive>/matrix/appservice-token` | one minter, on the authelia host | the hive process that presents the token to its homeserver, under the hive's own certificate | must be stated |
|
||||||
| `swarm/hives/<hive>/queue/agent` | authelia | the agent container presenting the OIDC client to the swarm queue, under its own certificate | must be stated |
|
| `swarm/hives/<hive>/queue/agent` | authelia | the agent container presenting the OIDC client to the swarm queue, under its own certificate | must be stated |
|
||||||
| `swarm/services/<clientId>/oidc/client` | authelia | the service process that presents the client secret, under the certificate of the host it runs on | must be stated |
|
| `swarm/services/<clientId>/oidc/client` | authelia | the service process that presents the client secret, under the certificate of the host it runs on | must be stated |
|
||||||
| `swarm/services/matrix/sender-token` | `swarm-matrix-minter`, in the `hive-matrix` container | `swarm-matrix-minter` itself, under its own certificate, before it decides whether to mint, and hive-c0re's `stored_sender_token()`, under the hive's own certificate | must be stated |
|
| `swarm/services/matrix/sender-token` | `swarm-matrix-ctl`, in the `hive-matrix` container | `swarm-matrix-ctl` itself, under its own certificate, before it decides whether to mint, and hive-c0re's `stored_sender_token()`, under the hive's own certificate | must be stated |
|
||||||
| _(not in the store)_ a hive's mTLS leaf | the store's own PKI, or an operator placing it by hand | its own client, off disk — the exception above, because it's what makes every other row's pull possible | must be stated |
|
| _(not in the store)_ a hive's mTLS leaf | the store's own PKI, or an operator placing it by hand | its own client, off disk — the exception above, because it's what makes every other row's pull possible | must be stated |
|
||||||
|
|
||||||
<!-- vale write-good.Passive = YES -->
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -156,9 +156,9 @@
|
||||||
services.hyperhive.deploy.nats.authPackage =
|
services.hyperhive.deploy.nats.authPackage =
|
||||||
lib.mkDefault
|
lib.mkDefault
|
||||||
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-nats-auth;
|
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-nats-auth;
|
||||||
services.hyperhive.deploy.matrix.minterPackage =
|
services.hyperhive.deploy.matrix.ctlPackage =
|
||||||
lib.mkDefault
|
lib.mkDefault
|
||||||
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-matrix-minter;
|
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-matrix-ctl;
|
||||||
services.hyperhive.deploy.authelia.bridgePackage =
|
services.hyperhive.deploy.authelia.bridgePackage =
|
||||||
lib.mkDefault
|
lib.mkDefault
|
||||||
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-authelia-bridge;
|
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-authelia-bridge;
|
||||||
|
|
|
||||||
|
|
@ -908,12 +908,12 @@ pub async fn sync_agent_standalone(name: &str) {
|
||||||
/// and is non-empty.
|
/// and is non-empty.
|
||||||
///
|
///
|
||||||
/// The token is taken from the **swarm secret store** when it is there:
|
/// The token is taken from the **swarm secret store** when it is there:
|
||||||
/// `swarm-matrix-minter`, the oneshot inside the matrix container, publishes
|
/// `swarm-matrix-ctl`, the oneshot inside the matrix container, publishes
|
||||||
/// it under an identity of its own, and taking it from there is what lets a
|
/// it under an identity of its own, and taking it from there is what lets a
|
||||||
/// hive that holds no `as_token` have an admin at all. The mint ladder below
|
/// hive that holds no `as_token` have an admin at all. The mint ladder below
|
||||||
/// stays as the fallback for a store that is empty, unconfigured or
|
/// stays as the fallback for a store that is empty, unconfigured or
|
||||||
/// unreachable — which is every swarm whose matrix container predates that
|
/// unreachable — which is every swarm whose matrix container predates that
|
||||||
/// minter.
|
/// binary.
|
||||||
pub async fn ensure_hive_user(client: &reqwest::Client, as_token: &str) -> Result<()> {
|
pub async fn ensure_hive_user(client: &reqwest::Client, as_token: &str) -> Result<()> {
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
let path = sender_token_path();
|
let path = sender_token_path();
|
||||||
|
|
@ -996,19 +996,19 @@ fn persist_sender_token(path: &std::path::Path, access_token: &str) -> Result<()
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch the sender token `swarm-matrix-minter` published, under
|
/// Fetch the sender token `swarm-matrix-ctl` published, under
|
||||||
/// this hive's own store identity.
|
/// this hive's own store identity.
|
||||||
///
|
///
|
||||||
/// The cert role is the hive's name, straight out of `HYPERHIVE_HIVE_NAME` —
|
/// The cert role is the hive's name, straight out of `HYPERHIVE_HIVE_NAME` —
|
||||||
/// the same role string `workers::credential` logs in with, and already in
|
/// the same role string `workers::credential` logs in with, and already in
|
||||||
/// this process's environment, so the store read costs no plumbing through
|
/// this process's environment, so the store read costs no plumbing through
|
||||||
/// [`ensure_all`]. No new grant either: a hive's policy already covers the
|
/// [`ensure_all`]. No new grant either: a hive's policy already covers the
|
||||||
/// whole `swarm/services/*` tree the minter writes into.
|
/// whole `swarm/services/*` tree `swarm-matrix-ctl` writes into.
|
||||||
///
|
///
|
||||||
/// `None`, never an error, for every way this can come up empty — no hive
|
/// `None`, never an error, for every way this can come up empty — no hive
|
||||||
/// name, no `BAO_*` identity, an unreachable store, nothing at the path. All
|
/// name, no `BAO_*` identity, an unreachable store, nothing at the path. All
|
||||||
/// four mean the same thing to the caller ("mint it the old way"), and three
|
/// four mean the same thing to the caller ("mint it the old way"), and three
|
||||||
/// of them are the ordinary state of a swarm that has not deployed the minter
|
/// of them are the ordinary state of a swarm that has not deployed `swarm-matrix-ctl`
|
||||||
/// yet, so raising would turn a supported deployment into a warning every
|
/// yet, so raising would turn a supported deployment into a warning every
|
||||||
/// sweep.
|
/// sweep.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
./glue-controller-bao-identity.nix
|
./glue-controller-bao-identity.nix
|
||||||
./glue-grafana-oidc-client.nix
|
./glue-grafana-oidc-client.nix
|
||||||
./glue-matrix-bao-token.nix
|
./glue-matrix-bao-token.nix
|
||||||
./glue-matrix-minter-bao-identity.nix
|
./glue-matrix-ctl-bao-identity.nix
|
||||||
./glue-queue-agent-credential.nix
|
./glue-queue-agent-credential.nix
|
||||||
./glue-secret-publisher-bao-identity.nix
|
./glue-secret-publisher-bao-identity.nix
|
||||||
./glue-swarm-otel-oidc-client.nix
|
./glue-swarm-otel-oidc-client.nix
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ in
|
||||||
[ -s ${pkiDir}/secret-publisher.pem ] || ${signLeaf} ${pkiDir} secret-publisher \
|
[ -s ${pkiDir}/secret-publisher.pem ] || ${signLeaf} ${pkiDir} secret-publisher \
|
||||||
${lib.escapeShellArg deployCfg.bao.secretPublisherCommonName} "" clientAuth
|
${lib.escapeShellArg deployCfg.bao.secretPublisherCommonName} "" clientAuth
|
||||||
|
|
||||||
# The matrix container's minter. Minted unconditionally like the two
|
# The matrix container's `swarm-matrix-ctl`. Minted unconditionally like the two
|
||||||
# above, and for the third variant of the same reason: the homeserver
|
# above, and for the third variant of the same reason: the homeserver
|
||||||
# is a swarm singleton, so on every hive but the one running it this
|
# is a swarm singleton, so on every hive but the one running it this
|
||||||
# leaf is the file an operator copies rather than a file anything
|
# leaf is the file an operator copies rather than a file anything
|
||||||
|
|
@ -158,8 +158,8 @@ in
|
||||||
# name and its policy reads the whole store, while this principal may
|
# name and its policy reads the whole store, while this principal may
|
||||||
# only write one path — which is the entire point of giving the
|
# only write one path — which is the entire point of giving the
|
||||||
# container an identity instead of lending it the hive's.
|
# container an identity instead of lending it the hive's.
|
||||||
[ -s ${pkiDir}/matrix-minter.pem ] || ${signLeaf} ${pkiDir} matrix-minter \
|
[ -s ${pkiDir}/matrix-ctl.pem ] || ${signLeaf} ${pkiDir} matrix-ctl \
|
||||||
${lib.escapeShellArg deployCfg.bao.matrixMinterCommonName} "" clientAuth
|
${lib.escapeShellArg deployCfg.bao.matrixCtlCommonName} "" clientAuth
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
# Glue: point the matrix container's minter at the bao leaf minted for it.
|
# Glue: point the matrix container's `swarm-matrix-ctl` at the bao leaf minted for it.
|
||||||
#
|
#
|
||||||
# ONE PAIRING PER FILE — minter ← bao, and nothing else. Deleting this leaves a
|
# ONE PAIRING PER FILE — matrix-ctl ← bao, and nothing else. Deleting this leaves a
|
||||||
# minter that takes operator-provided certificate paths, which is what any
|
# binary that takes operator-provided certificate paths, which is what any
|
||||||
# deployment not minting its own already does.
|
# deployment not minting its own already does.
|
||||||
#
|
#
|
||||||
# ⚠️ The minting is NOT here. ./glue-bao-tls.nix holds the CA and signs the
|
# ⚠️ The minting is NOT here. ./glue-bao-tls.nix holds the CA and signs the
|
||||||
# leaf, because the thing that owns a private key owns issuing from it. What
|
# leaf, because the thing that owns a private key owns issuing from it. What
|
||||||
# belongs here is the pairing: which paths the minter presents.
|
# belongs here is the pairing: which paths matrix-ctl presents.
|
||||||
#
|
#
|
||||||
# ⚠️ Gated on the leaf existing, not on the store being enabled — the same rule
|
# ⚠️ Gated on the leaf existing, not on the store being enabled — the same rule
|
||||||
# ./glue-secret-publisher-bao-identity.nix states, and it bites harder here: a
|
# ./glue-secret-publisher-bao-identity.nix states, and it bites harder here: a
|
||||||
|
|
@ -33,8 +33,8 @@ in
|
||||||
{
|
{
|
||||||
config = lib.mkIf (hyperhiveCfg.enable && deployCfg.matrix.enable && haveMintedPki) {
|
config = lib.mkIf (hyperhiveCfg.enable && deployCfg.matrix.enable && haveMintedPki) {
|
||||||
services.hyperhive.deploy.matrix = {
|
services.hyperhive.deploy.matrix = {
|
||||||
minterBaoClientCertFile = lib.mkDefault "${pkiDir}/matrix-minter.pem";
|
ctlBaoClientCertFile = lib.mkDefault "${pkiDir}/matrix-ctl.pem";
|
||||||
minterBaoClientKeyFile = lib.mkDefault "${pkiDir}/matrix-minter-key.pem";
|
ctlBaoClientKeyFile = lib.mkDefault "${pkiDir}/matrix-ctl-key.pem";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +113,7 @@ let
|
||||||
appserviceCredentialId = "${appserviceId}-appservice.yaml";
|
appserviceCredentialId = "${appserviceId}-appservice.yaml";
|
||||||
appserviceCredentialDir = "/run/credentials/tuwunel.service";
|
appserviceCredentialDir = "/run/credentials/tuwunel.service";
|
||||||
|
|
||||||
# ── swarm-matrix-minter ────────────────────────────────────────────────
|
# ── swarm-matrix-ctl ────────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
# The oneshot that publishes the appservice sender account's access token to
|
# The oneshot that publishes the appservice sender account's access token to
|
||||||
# the swarm's secret store. It runs INSIDE the container, beside tuwunel,
|
# the swarm's secret store. It runs INSIDE the container, beside tuwunel,
|
||||||
|
|
@ -127,16 +127,16 @@ let
|
||||||
# "co-located with bao" would leave the intended deployment silently minting
|
# "co-located with bao" would leave the intended deployment silently minting
|
||||||
# nothing. Same rule ./swarm-secret-publisher.nix's `haveClientIdentity`
|
# nothing. Same rule ./swarm-secret-publisher.nix's `haveClientIdentity`
|
||||||
# states, for a sharper reason.
|
# states, for a sharper reason.
|
||||||
minterActive =
|
ctlActive =
|
||||||
deployCfg.matrix.minterBaoClientCertFile != null && deployCfg.matrix.minterBaoClientKeyFile != null;
|
deployCfg.matrix.ctlBaoClientCertFile != null && deployCfg.matrix.ctlBaoClientKeyFile != null;
|
||||||
|
|
||||||
# The role on the store's `cert` auth mount, and so the single source of the
|
# The role on the store's `cert` auth mount, and so the single source of the
|
||||||
# string both ends must agree on: ./swarm-bao.nix writes the role under
|
# string both ends must agree on: ./swarm-bao.nix writes the role under
|
||||||
# `matrixMinterPolicyName` and this hands it to the binary in the
|
# `matrixCtlPolicyName` and this hands it to the binary in the
|
||||||
# environment, so the binary itself spells no role at all.
|
# environment, so the binary itself spells no role at all.
|
||||||
minterCertRole = "swarm-matrix-minter";
|
ctlCertRole = "swarm-matrix-ctl";
|
||||||
|
|
||||||
# Every host directory the minter's bao identity is spread across. Normally
|
# Every host directory matrix-ctl's bao identity is spread across. Normally
|
||||||
# one — ./glue-bao-tls.nix puts all three files in ./glue's PKI dir — but
|
# one — ./glue-bao-tls.nix puts all three files in ./glue's PKI dir — but
|
||||||
# derived rather than assumed, because an operator naming paths by hand is
|
# derived rather than assumed, because an operator naming paths by hand is
|
||||||
# exactly the deployment the gate above is written for.
|
# exactly the deployment the gate above is written for.
|
||||||
|
|
@ -144,29 +144,29 @@ let
|
||||||
# Directories rather than the files, for the reason `appserviceDir`'s own
|
# Directories rather than the files, for the reason `appserviceDir`'s own
|
||||||
# comment gives: a re-issued leaf is a new inode, and binding the file would
|
# comment gives: a re-issued leaf is a new inode, and binding the file would
|
||||||
# pin the one the container saw when it started.
|
# pin the one the container saw when it started.
|
||||||
minterPkiDirs = lib.optionals minterActive (
|
ctlPkiDirs = lib.optionals ctlActive (
|
||||||
lib.unique (
|
lib.unique (
|
||||||
map builtins.dirOf (
|
map builtins.dirOf (
|
||||||
[
|
[
|
||||||
deployCfg.matrix.minterBaoClientCertFile
|
deployCfg.matrix.ctlBaoClientCertFile
|
||||||
deployCfg.matrix.minterBaoClientKeyFile
|
deployCfg.matrix.ctlBaoClientKeyFile
|
||||||
]
|
]
|
||||||
++ lib.optional (deployCfg.bao.serverCaFile != null) deployCfg.bao.serverCaFile
|
++ lib.optional (deployCfg.bao.serverCaFile != null) deployCfg.bao.serverCaFile
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
minterBindMounts = lib.genAttrs minterPkiDirs (dir: {
|
ctlBindMounts = lib.genAttrs ctlPkiDirs (dir: {
|
||||||
hostPath = dir;
|
hostPath = dir;
|
||||||
isReadOnly = true;
|
isReadOnly = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
# Where a reader of the published credential is told the token is good for.
|
# Where a reader of the published credential is told the token is good for.
|
||||||
# Empty when this hive serves no vhost: `matrix::Credential.homeserver` is an
|
# Empty when this hive serves no vhost: `matrix::Credential.homeserver` is an
|
||||||
# `Option`, and the minter reads an empty variable as absent rather than as
|
# `Option`, and matrix-ctl reads an empty variable as absent rather than as
|
||||||
# the string "null" — which is what a hive with no gateway host actually
|
# the string "null" — which is what a hive with no gateway host actually
|
||||||
# knows about itself.
|
# knows about itself.
|
||||||
minterHomeserverUrl = if cfg.gatewayHost == null then "" else "https://${toString cfg.gatewayHost}";
|
ctlHomeserverUrl = if cfg.gatewayHost == null then "" else "https://${toString cfg.gatewayHost}";
|
||||||
|
|
||||||
# Every local user this hive may provision — agents, `@hive:` itself, and
|
# Every local user this hive may provision — agents, `@hive:` itself, and
|
||||||
# the operator accounts `hivectl matrix create-user` makes, which is the
|
# the operator accounts `hivectl matrix create-user` makes, which is the
|
||||||
|
|
@ -748,26 +748,26 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
minterPackage = lib.mkOption {
|
ctlPackage = lib.mkOption {
|
||||||
type = lib.types.package;
|
type = lib.types.package;
|
||||||
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-matrix-minter";
|
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-matrix-ctl";
|
||||||
description = ''
|
description = ''
|
||||||
The `swarm-matrix-minter` build run inside the matrix container.
|
The `swarm-matrix-ctl` build run inside the matrix container.
|
||||||
|
|
||||||
⚠️ Named `minterPackage`, not folded into `package` above: that one is
|
⚠️ Named `ctlPackage`, not folded into `package` above: that one is
|
||||||
the homeserver, and this is a hyperhive binary that happens to run
|
the homeserver, and this is a hyperhive binary that happens to run
|
||||||
beside it. Same split, and same reason, as
|
beside it. Same split, and same reason, as
|
||||||
{option}`services.hyperhive.deploy.nats.authPackage`.
|
{option}`services.hyperhive.deploy.nats.authPackage`.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
minterBaoClientCertFile = lib.mkOption {
|
ctlBaoClientCertFile = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Client certificate the matrix container's minter presents to the
|
Client certificate the matrix container's `swarm-matrix-ctl` presents to the
|
||||||
swarm's secret store. Its subject must be
|
swarm's secret store. Its subject must be
|
||||||
{option}`services.hyperhive.deploy.bao.matrixMinterCommonName` — cert
|
{option}`services.hyperhive.deploy.bao.matrixCtlCommonName` — cert
|
||||||
auth matches on the CN, and the role accepts nothing else.
|
auth matches on the CN, and the role accepts nothing else.
|
||||||
|
|
||||||
⚠️ **Not the hive's own leaf**, and that is the whole deliverable of
|
⚠️ **Not the hive's own leaf**, and that is the whole deliverable of
|
||||||
|
|
@ -778,7 +778,7 @@ in
|
||||||
|
|
||||||
No default: a module that guessed would be holding the CA opinion
|
No default: a module that guessed would be holding the CA opinion
|
||||||
./swarm-bao.nix deliberately does not hold.
|
./swarm-bao.nix deliberately does not hold.
|
||||||
./glue-matrix-minter-bao-identity.nix points it at the leaf
|
./glue-matrix-ctl-bao-identity.nix points it at the leaf
|
||||||
./glue-bao-tls.nix mints, where this host mints one.
|
./glue-bao-tls.nix mints, where this host mints one.
|
||||||
|
|
||||||
The file and its key are bind-mounted into the container read-only.
|
The file and its key are bind-mounted into the container read-only.
|
||||||
|
|
@ -788,13 +788,13 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
minterBaoClientKeyFile = lib.mkOption {
|
ctlBaoClientKeyFile = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Private key for
|
Private key for
|
||||||
{option}`services.hyperhive.deploy.matrix.minterBaoClientCertFile`.
|
{option}`services.hyperhive.deploy.matrix.ctlBaoClientCertFile`.
|
||||||
Both or neither — the minter unit does not exist unless each is set.
|
Both or neither — the unit does not exist unless each is set.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -1194,10 +1194,10 @@ in
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// caTrust.bindMount
|
// caTrust.bindMount
|
||||||
# The minter's bao client identity, and nothing else of the hive's. See
|
# matrix-ctl's bao client identity, and nothing else of the hive's. See
|
||||||
# `minterPkiDirs` above for why it is a derived set of directories
|
# `ctlPkiDirs` above for why it is a derived set of directories
|
||||||
# rather than one literal.
|
# rather than one literal.
|
||||||
// minterBindMounts;
|
// ctlBindMounts;
|
||||||
config =
|
config =
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
|
|
@ -1394,7 +1394,7 @@ in
|
||||||
|
|
||||||
# Publish the appservice sender account's access token to the swarm
|
# Publish the appservice sender account's access token to the swarm
|
||||||
# store, once, under an identity that belongs to this container and
|
# store, once, under an identity that belongs to this container and
|
||||||
# not to the hive. See `minterActive` above for why it runs here.
|
# not to the hive. See `ctlActive` above for why it runs here.
|
||||||
#
|
#
|
||||||
# A `oneshot` with no timer and no retry loop of its own: the whole
|
# A `oneshot` with no timer and no retry loop of its own: the whole
|
||||||
# of "and only once" is the binary's first act, a read of the path it
|
# of "and only once" is the binary's first act, a read of the path it
|
||||||
|
|
@ -1402,7 +1402,7 @@ in
|
||||||
# a homeserver still starting; `RemainAfterExit` is deliberately NOT
|
# a homeserver still starting; `RemainAfterExit` is deliberately NOT
|
||||||
# set, because the unit having succeeded is not the idempotency
|
# set, because the unit having succeeded is not the idempotency
|
||||||
# record — the store is, and it outlives this machine.
|
# record — the store is, and it outlives this machine.
|
||||||
systemd.services.swarm-matrix-minter = lib.mkIf minterActive {
|
systemd.services.swarm-matrix-ctl = lib.mkIf ctlActive {
|
||||||
description = "publish the matrix sender token to the swarm secret store";
|
description = "publish the matrix sender token to the swarm secret store";
|
||||||
# Ordered after the homeserver because both of the ladder's arms
|
# Ordered after the homeserver because both of the ladder's arms
|
||||||
# are client-server API calls. `wants`, not `requires`: a run that
|
# are client-server API calls. `wants`, not `requires`: a run that
|
||||||
|
|
@ -1414,29 +1414,32 @@ in
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
ExecStart = "${deployCfg.matrix.minterPackage}/bin/swarm-matrix-minter";
|
# The verb is part of the contract: `swarm-matrix-ctl` is a
|
||||||
|
# subcommand binary and refuses a bare invocation, so dropping
|
||||||
|
# `mint` here fails the unit rather than doing something else.
|
||||||
|
ExecStart = "${deployCfg.matrix.ctlPackage}/bin/swarm-matrix-ctl mint";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
RestartSec = 30;
|
RestartSec = 30;
|
||||||
# Bounded here rather than left to systemd's default, for the
|
# Bounded here rather than left to systemd's default, for the
|
||||||
# reason ./swarm-secret-publisher.nix states: a sealed store
|
# reason ./swarm-secret-publisher.nix states: a sealed store
|
||||||
# answers on the port and never answers the read.
|
# answers on the port and never answers the read.
|
||||||
TimeoutStartSec = 60;
|
TimeoutStartSec = 60;
|
||||||
SyslogIdentifier = "swarm-matrix-minter";
|
SyslogIdentifier = "swarm-matrix-ctl";
|
||||||
};
|
};
|
||||||
environment = {
|
environment = {
|
||||||
BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}";
|
BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}";
|
||||||
BAO_CLIENT_CERT = deployCfg.matrix.minterBaoClientCertFile;
|
BAO_CLIENT_CERT = deployCfg.matrix.ctlBaoClientCertFile;
|
||||||
BAO_CLIENT_KEY = deployCfg.matrix.minterBaoClientKeyFile;
|
BAO_CLIENT_KEY = deployCfg.matrix.ctlBaoClientKeyFile;
|
||||||
MATRIX_MINTER_CERT_ROLE = minterCertRole;
|
MATRIX_MINT_CERT_ROLE = ctlCertRole;
|
||||||
# Loopback: this container shares the host netns, so the
|
# Loopback: this container shares the host netns, so the
|
||||||
# homeserver it must talk to is the one in this very unit's
|
# homeserver it must talk to is the one in this very unit's
|
||||||
# netns and needs no name, no vhost and no TLS.
|
# netns and needs no name, no vhost and no TLS.
|
||||||
MATRIX_MINTER_API_URL = "http://127.0.0.1:${toString cfg.httpPort}";
|
MATRIX_MINT_API_URL = "http://127.0.0.1:${toString cfg.httpPort}";
|
||||||
# The bind-mounted registration, which IS the as_token. A path,
|
# The bind-mounted registration, which IS the as_token. A path,
|
||||||
# never a value.
|
# never a value.
|
||||||
MATRIX_MINTER_REGISTRATION = appserviceRegistrationPath;
|
MATRIX_MINT_REGISTRATION = appserviceRegistrationPath;
|
||||||
MATRIX_MINTER_LOCALPART = hiveLocalpart;
|
MATRIX_MINT_LOCALPART = hiveLocalpart;
|
||||||
MATRIX_MINTER_HOMESERVER = minterHomeserverUrl;
|
MATRIX_MINT_HOMESERVER = ctlHomeserverUrl;
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs (deployCfg.bao.serverCaFile != null) {
|
// lib.optionalAttrs (deployCfg.bao.serverCaFile != null) {
|
||||||
BAO_CACERT = deployCfg.bao.serverCaFile;
|
BAO_CACERT = deployCfg.bao.serverCaFile;
|
||||||
|
|
|
||||||
|
|
@ -236,12 +236,12 @@ let
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# The identity the matrix container's minter presents. Named outside `hive-*`
|
# The identity the matrix container's `swarm-matrix-ctl` presents. Named outside `hive-*`
|
||||||
# for the reason its two siblings above give — the controller may rewrite
|
# for the reason its two siblings above give — the controller may rewrite
|
||||||
# every policy under that prefix, and a policy it can rewrite constrains
|
# every policy under that prefix, and a policy it can rewrite constrains
|
||||||
# nothing.
|
# nothing.
|
||||||
matrixMinterPolicyName = "swarm-matrix-minter";
|
matrixCtlPolicyName = "swarm-matrix-ctl";
|
||||||
matrixMinterCn = baoDeploy.matrixMinterCommonName;
|
matrixCtlCn = baoDeploy.matrixCtlCommonName;
|
||||||
|
|
||||||
# ONE path, and every narrowing in it is load-bearing.
|
# ONE path, and every narrowing in it is load-bearing.
|
||||||
#
|
#
|
||||||
|
|
@ -254,12 +254,12 @@ let
|
||||||
# spelled to the leaf for that reason, not for tidiness.
|
# spelled to the leaf for that reason, not for tidiness.
|
||||||
#
|
#
|
||||||
# `read` as well as write, unlike either sibling, and it is what makes "and
|
# `read` as well as write, unlike either sibling, and it is what makes "and
|
||||||
# only once" mechanical: the minter's first act is to read this path back and
|
# only once" mechanical: matrix-ctl's first act is to read this path back and
|
||||||
# stop if something is there, so without the capability every container
|
# stop if something is there, so without the capability every container
|
||||||
# restart would mint a second access token and invalidate the hive's. A read
|
# restart would mint a second access token and invalidate the hive's. A read
|
||||||
# here recovers one secret this principal itself wrote, which is a much
|
# here recovers one secret this principal itself wrote, which is a much
|
||||||
# narrower grant than the publisher's would have been.
|
# narrower grant than the publisher's would have been.
|
||||||
matrixMinterPolicyText = ''
|
matrixCtlPolicyText = ''
|
||||||
path "${credentialMountPath}/data/swarm/services/matrix/sender-token" {
|
path "${credentialMountPath}/data/swarm/services/matrix/sender-token" {
|
||||||
capabilities = ["create", "update", "read"]
|
capabilities = ["create", "update", "read"]
|
||||||
}
|
}
|
||||||
|
|
@ -688,12 +688,12 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
matrixMinterCommonName = lib.mkOption {
|
matrixCtlCommonName = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "swarm-matrix-minter";
|
default = "swarm-matrix-ctl";
|
||||||
example = "swarm-matrix-minter.svc";
|
example = "swarm-matrix-ctl.svc";
|
||||||
description = ''
|
description = ''
|
||||||
Subject the store's matrix-minter cert-auth role accepts — the
|
Subject the store's matrix-ctl cert-auth role accepts — the
|
||||||
identity the oneshot inside the matrix container presents when it
|
identity the oneshot inside the matrix container presents when it
|
||||||
publishes the appservice sender account's access token.
|
publishes the appservice sender account's access token.
|
||||||
|
|
||||||
|
|
@ -707,7 +707,7 @@ in
|
||||||
⚠️ Same collision as its siblings, and the same answer: ./swarm.nix
|
⚠️ Same collision as its siblings, and the same answer: ./swarm.nix
|
||||||
feeds this value into the guard on
|
feeds this value into the guard on
|
||||||
{option}`services.hyperhive.swarm.hives`, so a hive named after it
|
{option}`services.hyperhive.swarm.hives`, so a hive named after it
|
||||||
fails evaluation rather than silently receiving the minter's grant.
|
fails evaluation rather than silently receiving matrix-ctl's grant.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1133,7 +1133,7 @@ in
|
||||||
# ⚠️ Deliberately EMPTY: nothing here generates an issuer into it, so
|
# ⚠️ Deliberately EMPTY: nothing here generates an issuer into it, so
|
||||||
# the role below can be written but cannot yet issue. That is the
|
# the role below can be written but cannot yet issue. That is the
|
||||||
# whole shape of this step — the mount, its narrowing and its grant
|
# whole shape of this step — the mount, its narrowing and its grant
|
||||||
# exist, and the minter that uses them arrives separately. Undoing it
|
# exist, and the binary that uses them arrives separately. Undoing it
|
||||||
# is `bao secrets disable ${servicesPkiMountPath}`.
|
# is `bao secrets disable ${servicesPkiMountPath}`.
|
||||||
case "$mounts" in
|
case "$mounts" in
|
||||||
*'"${servicesPkiMountPath}/"'*) ;;
|
*'"${servicesPkiMountPath}/"'*) ;;
|
||||||
|
|
@ -1157,7 +1157,7 @@ in
|
||||||
# is a name type this issuer is unconstrained for.
|
# is a name type this issuer is unconstrained for.
|
||||||
# `server_flag` alone because that is what today's leaf carries
|
# `server_flag` alone because that is what today's leaf carries
|
||||||
# (`extendedKeyUsage=serverAuth`, ./hive-tls.nix:144), and rsa:4096
|
# (`extendedKeyUsage=serverAuth`, ./hive-tls.nix:144), and rsa:4096
|
||||||
# because that is the key the current minter generates.
|
# because that is the key the current implementation generates.
|
||||||
#
|
#
|
||||||
# An empty `swarm.serviceDomains` renders an empty `allowed_domains`,
|
# An empty `swarm.serviceDomains` renders an empty `allowed_domains`,
|
||||||
# which with `allow_any_name=false` is a role that issues nothing —
|
# which with `allow_any_name=false` is a role that issues nothing —
|
||||||
|
|
@ -1281,8 +1281,8 @@ in
|
||||||
# creates the mounts this one writes into, but a failed oneshot still
|
# creates the mounts this one writes into, but a failed oneshot still
|
||||||
# counts as finished, so only ordering plus this unit's own retry
|
# counts as finished, so only ordering plus this unit's own retry
|
||||||
# converges.
|
# converges.
|
||||||
systemd.services.swarm-bao-matrix-minter-policy = lib.mkIf haveBootstrapToken {
|
systemd.services.swarm-bao-matrix-ctl-policy = lib.mkIf haveBootstrapToken {
|
||||||
description = "write the swarm matrix minter's bao policy and cert-auth role";
|
description = "write swarm-matrix-ctl's bao policy and cert-auth role";
|
||||||
after = [
|
after = [
|
||||||
"container@${cfg.machine}.service"
|
"container@${cfg.machine}.service"
|
||||||
"swarm-bao-controller-policy.service"
|
"swarm-bao-controller-policy.service"
|
||||||
|
|
@ -1309,16 +1309,16 @@ in
|
||||||
BAO_TOKEN="$(cat ${lib.escapeShellArg baoDeploy.bootstrapTokenFile})"
|
BAO_TOKEN="$(cat ${lib.escapeShellArg baoDeploy.bootstrapTokenFile})"
|
||||||
export BAO_TOKEN
|
export BAO_TOKEN
|
||||||
|
|
||||||
printf '%s' ${lib.escapeShellArg matrixMinterPolicyText} |
|
printf '%s' ${lib.escapeShellArg matrixCtlPolicyText} |
|
||||||
bao policy write ${lib.escapeShellArg matrixMinterPolicyName} -
|
bao policy write ${lib.escapeShellArg matrixCtlPolicyName} -
|
||||||
''
|
''
|
||||||
+ lib.optionalString (baoDeploy.clientCaFile != null) ''
|
+ lib.optionalString (baoDeploy.clientCaFile != null) ''
|
||||||
|
|
||||||
bao write auth/cert/certs/${lib.escapeShellArg matrixMinterPolicyName} \
|
bao write auth/cert/certs/${lib.escapeShellArg matrixCtlPolicyName} \
|
||||||
certificate=@${tlsDir}/client-ca.pem \
|
certificate=@${tlsDir}/client-ca.pem \
|
||||||
allowed_common_names=${lib.escapeShellArg matrixMinterCn} \
|
allowed_common_names=${lib.escapeShellArg matrixCtlCn} \
|
||||||
token_policies=${lib.escapeShellArg matrixMinterPolicyName} \
|
token_policies=${lib.escapeShellArg matrixCtlPolicyName} \
|
||||||
display_name=${lib.escapeShellArg matrixMinterCn}
|
display_name=${lib.escapeShellArg matrixCtlCn}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ let
|
||||||
certAuthCns = [
|
certAuthCns = [
|
||||||
deployCfg.bao.controllerCommonName
|
deployCfg.bao.controllerCommonName
|
||||||
deployCfg.bao.secretPublisherCommonName
|
deployCfg.bao.secretPublisherCommonName
|
||||||
deployCfg.bao.matrixMinterCommonName
|
deployCfg.bao.matrixCtlCommonName
|
||||||
];
|
];
|
||||||
|
|
||||||
# Public hostnames of the swarm's own services, in declaration order.
|
# Public hostnames of the swarm's own services, in declaration order.
|
||||||
|
|
|
||||||
|
|
@ -152,10 +152,10 @@ let
|
||||||
# `Kind::label`, which renders the singular for error text. The singular
|
# `Kind::label`, which renders the singular for error text. The singular
|
||||||
# spelling evaluates, deploys, and 403s every read with "permission
|
# spelling evaluates, deploys, and 403s every read with "permission
|
||||||
# denied" and nothing else.
|
# denied" and nothing else.
|
||||||
name = "the matrix minter's grant is the sender token's path and nothing else";
|
name = "matrix-ctl's grant is the sender token's path and nothing else";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
s = baoGrantHere.systemd.services.swarm-bao-matrix-minter-policy.script;
|
s = baoGrantHere.systemd.services.swarm-bao-matrix-ctl-policy.script;
|
||||||
in
|
in
|
||||||
lib.hasInfix "path \"secret/data/swarm/services/matrix/sender-token\" {" s
|
lib.hasInfix "path \"secret/data/swarm/services/matrix/sender-token\" {" s
|
||||||
&& !(lib.hasInfix "secret/data/swarm/services/*" s)
|
&& !(lib.hasInfix "secret/data/swarm/services/*" s)
|
||||||
|
|
@ -165,30 +165,30 @@ let
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# 🩸 `read` is load-bearing here and is the one capability neither
|
# 🩸 `read` is load-bearing here and is the one capability neither
|
||||||
# sibling has. The minter's first act is to read this path back and stop
|
# sibling has. matrix-ctl's first act is to read this path back and stop
|
||||||
# if something is there — that read IS "and only once", so without the
|
# if something is there — that read IS "and only once", so without the
|
||||||
# capability every container restart would mint a second access token and
|
# capability every container restart would mint a second access token and
|
||||||
# invalidate the hive's.
|
# invalidate the hive's.
|
||||||
name = "the matrix minter may read back the one path it writes";
|
name = "matrix-ctl may read back the one path it writes";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
s = baoGrantHere.systemd.services.swarm-bao-matrix-minter-policy.script;
|
s = baoGrantHere.systemd.services.swarm-bao-matrix-ctl-policy.script;
|
||||||
in
|
in
|
||||||
lib.hasInfix "capabilities = [\"create\", \"update\", \"read\"]" s
|
lib.hasInfix "capabilities = [\"create\", \"update\", \"read\"]" s
|
||||||
&& lib.hasInfix "auth/cert/certs/swarm-matrix-minter" s
|
&& lib.hasInfix "auth/cert/certs/swarm-matrix-ctl" s
|
||||||
&& lib.hasInfix "allowed_common_names=swarm-matrix-minter" s;
|
&& lib.hasInfix "allowed_common_names=swarm-matrix-ctl" s;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# Same two controls its siblings carry: ordered after the unit that makes
|
# Same two controls its siblings carry: ordered after the unit that makes
|
||||||
# the mounts it writes into, and rendered on the HOST rather than inside
|
# the mounts it writes into, and rendered on the HOST rather than inside
|
||||||
# the store's container, where it would have neither an identity nor a
|
# the store's container, where it would have neither an identity nor a
|
||||||
# route to the store.
|
# route to the store.
|
||||||
name = "the minter's granting unit is ordered after the mounts and rendered on the host";
|
name = "matrix-ctl's granting unit is ordered after the mounts and rendered on the host";
|
||||||
ok =
|
ok =
|
||||||
lib.elem "swarm-bao-controller-policy.service" (
|
lib.elem "swarm-bao-controller-policy.service" (
|
||||||
baoGrantHere.systemd.services.swarm-bao-matrix-minter-policy.after
|
baoGrantHere.systemd.services.swarm-bao-matrix-ctl-policy.after
|
||||||
)
|
)
|
||||||
&& !(baoGrantHere.containers.swarm-bao.config.systemd.services ? swarm-bao-matrix-minter-policy);
|
&& !(baoGrantHere.containers.swarm-bao.config.systemd.services ? swarm-bao-matrix-ctl-policy);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# The policy authorising this route lives in another file, and nothing
|
# The policy authorising this route lives in another file, and nothing
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
# A homeserver on a hive with NO store identity at all — neither a local
|
# A homeserver on a hive with NO store identity at all — neither a local
|
||||||
# store nor a hand-placed leaf. The absence arm for the minter cases below
|
# store nor a hand-placed leaf. The absence arm for the matrix-ctl cases below
|
||||||
# needs it, and defining it here rather than importing keeps each group's
|
# needs it, and defining it here rather than importing keeps each group's
|
||||||
# fixture set its own, as ./lib.nix asks.
|
# fixture set its own, as ./lib.nix asks.
|
||||||
matrixNoBaoIdentity = hive { deploy.matrix.enable = true; };
|
matrixNoBaoIdentity = hive { deploy.matrix.enable = true; };
|
||||||
|
|
@ -230,30 +230,29 @@ let
|
||||||
# see from outside: it is consumed by a unit INSIDE a container, so a
|
# see from outside: it is consumed by a unit INSIDE a container, so a
|
||||||
# missing pairing renders as a container that comes up fine and publishes
|
# missing pairing renders as a container that comes up fine and publishes
|
||||||
# nothing.
|
# nothing.
|
||||||
name = "the store mints a leaf for the matrix minter, and the container is pointed at it";
|
name = "the store mints a leaf for matrix-ctl, and the container is pointed at it";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
m = baoWithMatrix;
|
m = baoWithMatrix;
|
||||||
p = m.services.hyperhive.deploy.matrix;
|
p = m.services.hyperhive.deploy.matrix;
|
||||||
in
|
in
|
||||||
lib.hasInfix "matrix-minter.pem" m.systemd.services.swarm-bao-pki.script
|
lib.hasInfix "matrix-ctl.pem" m.systemd.services.swarm-bao-pki.script
|
||||||
&& p.minterBaoClientCertFile == "/var/lib/swarm-bao-pki/matrix-minter.pem"
|
&& p.ctlBaoClientCertFile == "/var/lib/swarm-bao-pki/matrix-ctl.pem"
|
||||||
&& p.minterBaoClientKeyFile == "/var/lib/swarm-bao-pki/matrix-minter-key.pem";
|
&& p.ctlBaoClientKeyFile == "/var/lib/swarm-bao-pki/matrix-ctl-key.pem";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# 🩸 The identity separation this whole arrangement buys, stated as the
|
# 🩸 The identity separation this whole arrangement buys, stated as the
|
||||||
# one thing that would silently undo it. The container gets the MINTER's
|
# one thing that would silently undo it. The container gets MATRIX-CTL's
|
||||||
# leaf — whose grant is a single path — and not the hive's, which reads
|
# leaf — whose grant is a single path — and not the hive's, which reads
|
||||||
# every secret in the store. Both files exist in the same directory and
|
# every secret in the store. Both files exist in the same directory and
|
||||||
# both would evaluate, deploy and work.
|
# both would evaluate, deploy and work.
|
||||||
name = "the matrix minter presents its own leaf, never the hive's store-wide one";
|
name = "matrix-ctl presents its own leaf, never the hive's store-wide one";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
env = baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-minter.environment;
|
env = baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-ctl.environment;
|
||||||
hiveLeaf = baoWithMatrix.services.hyperhive.deploy.bao.clientCertFile;
|
hiveLeaf = baoWithMatrix.services.hyperhive.deploy.bao.clientCertFile;
|
||||||
in
|
in
|
||||||
env.BAO_CLIENT_CERT == "/var/lib/swarm-bao-pki/matrix-minter.pem"
|
env.BAO_CLIENT_CERT == "/var/lib/swarm-bao-pki/matrix-ctl.pem" && env.BAO_CLIENT_CERT != hiveLeaf;
|
||||||
&& env.BAO_CLIENT_CERT != hiveLeaf;
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# The bind mount is what makes the environment above resolvable: without
|
# The bind mount is what makes the environment above resolvable: without
|
||||||
|
|
@ -264,7 +263,7 @@ let
|
||||||
# The second arm is the shape guard: `bindMounts` is one literal plus two
|
# The second arm is the shape guard: `bindMounts` is one literal plus two
|
||||||
# merges, and a rewrite that dropped the appservice registration would
|
# merges, and a rewrite that dropped the appservice registration would
|
||||||
# take the homeserver's own credential with it.
|
# take the homeserver's own credential with it.
|
||||||
name = "the matrix container binds the minter's PKI read-only, without losing the appservice registration";
|
name = "the matrix container binds matrix-ctl's PKI read-only, without losing the appservice registration";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
mounts = baoWithMatrix.containers.hive-matrix.bindMounts;
|
mounts = baoWithMatrix.containers.hive-matrix.bindMounts;
|
||||||
|
|
@ -278,28 +277,43 @@ let
|
||||||
# the cert role ./host-modules/swarm-bao.nix writes, and a homeserver
|
# the cert role ./host-modules/swarm-bao.nix writes, and a homeserver
|
||||||
# address that is loopback because the container shares the host netns. A
|
# address that is loopback because the container shares the host netns. A
|
||||||
# vhost here would be a request out through the gateway and back.
|
# vhost here would be a request out through the gateway and back.
|
||||||
name = "the matrix minter is handed the store role and the loopback homeserver";
|
name = "matrix-ctl is handed the store role and the loopback homeserver";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
m = baoWithMatrix;
|
m = baoWithMatrix;
|
||||||
u = m.containers.hive-matrix.config.systemd.services.swarm-matrix-minter;
|
u = m.containers.hive-matrix.config.systemd.services.swarm-matrix-ctl;
|
||||||
port = m.services.hyperhive.swarm.matrix.httpPort;
|
port = m.services.hyperhive.swarm.matrix.httpPort;
|
||||||
in
|
in
|
||||||
u.environment.MATRIX_MINTER_CERT_ROLE == "swarm-matrix-minter"
|
u.environment.MATRIX_MINT_CERT_ROLE == "swarm-matrix-ctl"
|
||||||
&& u.environment.MATRIX_MINTER_API_URL == "http://127.0.0.1:${toString port}"
|
&& u.environment.MATRIX_MINT_API_URL == "http://127.0.0.1:${toString port}"
|
||||||
&& u.environment.MATRIX_MINTER_REGISTRATION == "/var/lib/hyperhive/matrix-appservice/hyperhive.yaml"
|
&& u.environment.MATRIX_MINT_REGISTRATION == "/var/lib/hyperhive/matrix-appservice/hyperhive.yaml"
|
||||||
&& u.serviceConfig.Type == "oneshot";
|
&& u.serviceConfig.Type == "oneshot";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
# 🩸 The crate is a `*ctl` with subcommands, so the unit has to name a
|
||||||
|
# VERB. This is the one end of that contract nix owns: the binary's own
|
||||||
|
# test pins how `mint` is spelled, but only a rendered `ExecStart` can
|
||||||
|
# say the unit actually passes it. A bare invocation exits non-zero with
|
||||||
|
# clap's usage — which is a deploy-time failure with no local signal, and
|
||||||
|
# exactly what the next verb added here is most likely to disturb.
|
||||||
|
name = "the unit invokes a verb rather than the bare binary";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
exec =
|
||||||
|
baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-ctl.serviceConfig.ExecStart;
|
||||||
|
in
|
||||||
|
lib.hasSuffix "/bin/swarm-matrix-ctl mint" exec;
|
||||||
|
}
|
||||||
{
|
{
|
||||||
# 🩸 A secret is a path, never a value — checked on the one unit in this
|
# 🩸 A secret is a path, never a value — checked on the one unit in this
|
||||||
# tree whose whole job is an `as_token`. Every variable it is given names
|
# tree whose whole job is an `as_token`. Every variable it is given names
|
||||||
# a file or an address; the token itself is read out of the bind-mounted
|
# a file or an address; the token itself is read out of the bind-mounted
|
||||||
# registration at runtime, so nothing here can be a token and an
|
# registration at runtime, so nothing here can be a token and an
|
||||||
# environment block is world-readable through `systemctl show`.
|
# environment block is world-readable through `systemctl show`.
|
||||||
name = "the matrix minter's environment carries paths and addresses, never a token";
|
name = "matrix-ctl's environment carries paths and addresses, never a token";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
env = baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-minter.environment;
|
env = baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-ctl.environment;
|
||||||
in
|
in
|
||||||
!(lib.any (v: lib.hasInfix "as_token" v || lib.hasInfix "syt_" v) (lib.attrValues env));
|
!(lib.any (v: lib.hasInfix "as_token" v || lib.hasInfix "syt_" v) (lib.attrValues env));
|
||||||
}
|
}
|
||||||
|
|
@ -307,12 +321,12 @@ let
|
||||||
# The absence arm, and the deployment it protects: a homeserver on a hive
|
# The absence arm, and the deployment it protects: a homeserver on a hive
|
||||||
# with no store identity at all. Without it the unit would exist naming
|
# with no store identity at all. Without it the unit would exist naming
|
||||||
# `null` as its certificate, which nixos renders as the literal string.
|
# `null` as its certificate, which nixos renders as the literal string.
|
||||||
name = "a matrix container with no store identity runs no minter and binds no PKI";
|
name = "a matrix container with no store identity runs no matrix-ctl and binds no PKI";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
units = matrixNoBaoIdentity.containers.hive-matrix.config.systemd.services;
|
units = matrixNoBaoIdentity.containers.hive-matrix.config.systemd.services;
|
||||||
in
|
in
|
||||||
!(units ? swarm-matrix-minter)
|
!(units ? swarm-matrix-ctl)
|
||||||
&& !(matrixNoBaoIdentity.containers.hive-matrix.bindMounts ? "/var/lib/swarm-bao-pki");
|
&& !(matrixNoBaoIdentity.containers.hive-matrix.bindMounts ? "/var/lib/swarm-bao-pki");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -53,13 +53,13 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
# The THIRD element of the same list, colliding on its own so neither of the
|
# The THIRD element of the same list, colliding on its own so neither of the
|
||||||
# two above can carry it. The minter's grant is one path rather than a whole
|
# two above can carry it. matrix-ctl's grant is one path rather than a whole
|
||||||
# prefix, which is exactly why a dead entry here would be easy to miss: a
|
# prefix, which is exactly why a dead entry here would be easy to miss: a
|
||||||
# hive that inherited it would not obviously break anything, it would
|
# hive that inherited it would not obviously break anything, it would
|
||||||
# silently gain the ability to overwrite the swarm's matrix credential.
|
# silently gain the ability to overwrite the swarm's matrix credential.
|
||||||
hiveNamedAfterMinterSubject = hive {
|
hiveNamedAfterMatrixCtlSubject = hive {
|
||||||
deploy.swarm-otel.enable = false;
|
deploy.swarm-otel.enable = false;
|
||||||
deploy.bao.matrixMinterCommonName = "mintctl";
|
deploy.bao.matrixCtlCommonName = "mintctl";
|
||||||
swarm.hives.mintctl.domain = "m.t.local";
|
swarm.hives.mintctl.domain = "m.t.local";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -104,12 +104,12 @@ let
|
||||||
# element earlier. `certAuthCns` is where a role added beside the others
|
# element earlier. `certAuthCns` is where a role added beside the others
|
||||||
# has to register itself, and nothing but a case per element notices when
|
# has to register itself, and nothing but a case per element notices when
|
||||||
# one forgets.
|
# one forgets.
|
||||||
name = "a hive named after the matrix minter's subject is refused too";
|
name = "a hive named after matrix-ctl's subject is refused too";
|
||||||
ok =
|
ok =
|
||||||
equalityGuardFired hiveNamedAfterMinterSubject
|
equalityGuardFired hiveNamedAfterMatrixCtlSubject
|
||||||
&& lib.any (
|
&& lib.any (
|
||||||
a: !a.assertion && lib.hasInfix "'mintctl'" a.message
|
a: !a.assertion && lib.hasInfix "'mintctl'" a.message
|
||||||
) hiveNamedAfterMinterSubject.assertions;
|
) hiveNamedAfterMatrixCtlSubject.assertions;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# Without this the case above proves nothing: an arm that fires for every
|
# Without this the case above proves nothing: an arm that fires for every
|
||||||
|
|
|
||||||
|
|
@ -163,12 +163,12 @@ in
|
||||||
# rather than every hive's.
|
# rather than every hive's.
|
||||||
swarm-nats-auth = mkBinPackage "swarm-nats-auth" "hyperhive swarm queue auth-callout responder";
|
swarm-nats-auth = mkBinPackage "swarm-nats-auth" "hyperhive swarm queue auth-callout responder";
|
||||||
|
|
||||||
# The appservice sender account's access-token minter. Out of `daemonBins` for
|
# The matrix container's control binary; `mint` publishes the sender token. Out of `daemonBins` for
|
||||||
# "runs *inside* a container, not on the host" reason as the responder
|
# "runs *inside* a container, not on the host" reason as the responder
|
||||||
# above, and with a second one: putting it in the core bundle would place
|
# above, and with a second one: putting it in the core bundle would place
|
||||||
# the binary that reads the appservice token on every hive's filesystem,
|
# the binary that reads the appservice token on every hive's filesystem,
|
||||||
# which is the arrangement it exists to end.
|
# which is the arrangement it exists to end.
|
||||||
swarm-matrix-minter = mkBinPackage "swarm-matrix-minter" "hyperhive matrix appservice sender-token minter";
|
swarm-matrix-ctl = mkBinPackage "swarm-matrix-ctl" "hyperhive matrix container control binary";
|
||||||
|
|
||||||
# The only process allowed to write swarm-authelia's users database —
|
# The only process allowed to write swarm-authelia's users database —
|
||||||
# same "runs *inside* a container, not on the host" placement as
|
# same "runs *inside* a container, not on the host" placement as
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
[package]
|
[package]
|
||||||
name = "swarm-matrix-minter"
|
name = "swarm-matrix-ctl"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "swarm-matrix-minter"
|
name = "swarm-matrix-ctl"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
# One verb today (`mint`), and the reason this crate is a `*ctl` rather than a
|
||||||
|
# single-purpose binary: the next thing that has to run in the matrix container
|
||||||
|
# is a subcommand here, not a new crate.
|
||||||
|
clap.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
# The agreement this binary is one end of: where the credential lives, what the
|
# The agreement this binary is one end of: where the credential lives, what the
|
||||||
|
|
@ -1,12 +1,30 @@
|
||||||
# swarm-matrix-minter
|
# swarm-matrix-ctl
|
||||||
|
|
||||||
A boot-time oneshot that runs **inside `containers.hive-matrix`**, beside the
|
The rust that runs **inside `containers.hive-matrix`**, beside the homeserver.
|
||||||
homeserver, and puts the appservice sender account's access token into the swarm's secret
|
|
||||||
store under an identity of its own.
|
|
||||||
|
|
||||||
## Why it lives in the matrix container
|
One binary with subcommands rather than one binary per job. Running code in that
|
||||||
|
container is not free: it needs its own store identity, its own cert role and
|
||||||
|
its own bind mounts, and every one of those is per-_container_, not per-task. A
|
||||||
|
second single-purpose crate would have had to duplicate that plumbing to add one
|
||||||
|
action, so the next thing that has to run in here is a **verb**, not a new
|
||||||
|
crate.
|
||||||
|
|
||||||
The credential it mints is authorised by the appservice `as_token`, and the
|
## Verbs
|
||||||
|
|
||||||
|
### `mint`
|
||||||
|
|
||||||
|
Puts the appservice sender account's access token into the swarm's secret store,
|
||||||
|
under an identity of its own. A boot-time oneshot.
|
||||||
|
|
||||||
|
Configured entirely by the `MATRIX_MINT_*` environment the unit sets — no flags.
|
||||||
|
A systemd `Environment=` block is what a nix module can render; a command line
|
||||||
|
full of paths is not. The prefix is scoped to the verb rather than to the binary
|
||||||
|
so the next verb brings its own, instead of widening a shared one nobody can
|
||||||
|
then narrow.
|
||||||
|
|
||||||
|
## Why this lives in the matrix container
|
||||||
|
|
||||||
|
The credential `mint` writes is authorised by the appservice `as_token`, and the
|
||||||
container already holds that: `nix/host-modules/hive-matrix.nix` bind-mounts the
|
container already holds that: `nix/host-modules/hive-matrix.nix` bind-mounts the
|
||||||
rendered appservice registration into it read-only, because that is how tuwunel
|
rendered appservice registration into it read-only, because that is how tuwunel
|
||||||
itself is handed the registration. Minting anywhere else would mean copying the
|
itself is handed the registration. Minting anywhere else would mean copying the
|
||||||
|
|
@ -14,13 +32,13 @@ itself is handed the registration. Minting anywhere else would mean copying the
|
||||||
stops being one.
|
stops being one.
|
||||||
|
|
||||||
It is not the swarm controller for the same reason, plus a structural one: a
|
It is not the swarm controller for the same reason, plus a structural one: a
|
||||||
homeserver has exactly **one** `@hive:` account and a swarm runs one
|
homeserver has exactly **one** appservice registration and so one sender
|
||||||
homeserver, so "mint it once" needs no lock, no lease and no trigger surface — it
|
account, and a swarm runs one homeserver, so "mint it once" needs no lock, no
|
||||||
is a property of the thing being minted.
|
lease and no trigger surface — it is a property of the thing being minted.
|
||||||
|
|
||||||
## Idempotency
|
## Idempotency
|
||||||
|
|
||||||
The **store** is the key, not the homeserver. A run reads
|
The **store** is the key, not the homeserver. A `mint` run reads
|
||||||
`swarm/services/matrix/sender-token` first and returns without touching the
|
`swarm/services/matrix/sender-token` first and returns without touching the
|
||||||
homeserver when something is already there. Only an empty path reaches the mint
|
homeserver when something is already there. Only an empty path reaches the mint
|
||||||
ladder:
|
ladder:
|
||||||
96
swarm-matrix-ctl/src/main.rs
Normal file
96
swarm-matrix-ctl/src/main.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
//! `swarm-matrix-ctl` — the rust that runs *inside* `containers.hive-matrix`.
|
||||||
|
//!
|
||||||
|
//! One binary with subcommands rather than one binary per job. The container
|
||||||
|
//! is an awkward place to put code — it needs its own store identity, its own
|
||||||
|
//! bind mounts and its own cert role — and all of that is per-*container*, not
|
||||||
|
//! per-task. A second single-purpose crate would have had to duplicate the
|
||||||
|
//! identity plumbing to add one action, so the next thing that has to run in
|
||||||
|
//! here is a verb below, not a new crate.
|
||||||
|
//!
|
||||||
|
//! Today that is one verb, [`mint`]: publish the appservice sender account's
|
||||||
|
//! homeserver access token to the swarm's secret store, once.
|
||||||
|
//!
|
||||||
|
//! It lives in the container because the appservice `as_token` that authorises
|
||||||
|
//! the mint is *already* there — the registration tuwunel loads is bind-mounted
|
||||||
|
//! in — so no second holder of that secret is created.
|
||||||
|
//!
|
||||||
|
//! 🩸 **A secret is a path, never a value.** The only identifier any verb here
|
||||||
|
//! logs is the store path; see `homeserver`'s module doc for the same rule
|
||||||
|
//! applied to error messages.
|
||||||
|
|
||||||
|
mod homeserver;
|
||||||
|
mod mint;
|
||||||
|
mod registration;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(
|
||||||
|
name = "swarm-matrix-ctl",
|
||||||
|
about = "Act on the swarm's matrix homeserver from inside its container"
|
||||||
|
)]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
enum Command {
|
||||||
|
/// Publish the appservice sender account's access token to the swarm
|
||||||
|
/// secret store, once.
|
||||||
|
///
|
||||||
|
/// Configured entirely by the `MATRIX_MINT_*` environment the unit sets —
|
||||||
|
/// no flags, because a systemd `Environment=` block is what a nix module
|
||||||
|
/// can render and a command line full of paths is not.
|
||||||
|
Mint,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
match Cli::parse().command {
|
||||||
|
Command::Mint => mint::run().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use clap::CommandFactory;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_clap_tree_is_well_formed() {
|
||||||
|
Cli::command().debug_assert();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The unit's `ExecStart` names a verb, so a rename of it is a deploy-time
|
||||||
|
/// failure with no local signal. This is that signal.
|
||||||
|
#[test]
|
||||||
|
fn mint_is_spelled_the_way_the_unit_invokes_it() {
|
||||||
|
let cli = Cli::try_parse_from(["swarm-matrix-ctl", "mint"]).expect("`mint` is a verb");
|
||||||
|
assert!(matches!(cli.command, Command::Mint));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The control: without it the case above passes on a parser that accepts
|
||||||
|
/// anything.
|
||||||
|
#[test]
|
||||||
|
fn an_unknown_verb_is_refused() {
|
||||||
|
Cli::try_parse_from(["swarm-matrix-ctl", "conjure"])
|
||||||
|
.expect_err("only declared verbs are accepted");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A bare invocation must not silently do something. `mint` writes a
|
||||||
|
/// credential, so "no verb" defaulting to it would make a typo in the unit
|
||||||
|
/// mint rather than fail.
|
||||||
|
#[test]
|
||||||
|
fn no_verb_at_all_is_refused() {
|
||||||
|
Cli::try_parse_from(["swarm-matrix-ctl"]).expect_err("a verb is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,25 +1,16 @@
|
||||||
//! Mint the matrix appservice sender account's homeserver access token, once, and publish it to the
|
//! `swarm-matrix-ctl mint` — publish the appservice sender account's
|
||||||
//! swarm's secret store.
|
//! homeserver access token to the swarm's secret store, once.
|
||||||
//!
|
//!
|
||||||
//! A oneshot inside `containers.hive-matrix`, not a daemon and not part of the
|
//! A oneshot inside `containers.hive-matrix`, not a daemon and not part of the
|
||||||
//! swarm controller. It is in the container because the appservice `as_token`
|
//! swarm controller. It is this account rather than an agent's because a
|
||||||
//! that authorises the mint is *already* there — the registration tuwunel loads
|
//! homeserver has **one** appservice registration and so one sender account,
|
||||||
//! is bind-mounted in — so no second holder of that secret is created. It is
|
//! and a swarm runs one homeserver: "only once" is a property of what is being
|
||||||
//! this account rather than an agent's because a homeserver has **one** `@hive:`
|
//! minted, so there is nothing to lock and no trigger to serve.
|
||||||
//! account and a swarm runs one homeserver: "only once" is a property of what
|
|
||||||
//! is being minted, so there is nothing to lock and no trigger to serve.
|
|
||||||
//!
|
//!
|
||||||
//! The store, not the homeserver, is the idempotency key — see
|
//! The store, not the homeserver, is the idempotency key — see
|
||||||
//! [`already_published`]. On the hive side `hive-c0re`'s
|
//! [`already_published`]. On the hive side `hive-c0re`'s
|
||||||
//! `matrix::ensure_hive_user` reads exactly the path written here, which is how
|
//! `matrix::ensure_hive_user` reads exactly the path written here, which is how
|
||||||
//! a hive that holds no `as_token` still gets its matrix account.
|
//! a hive that holds no `as_token` still gets its matrix account.
|
||||||
//!
|
|
||||||
//! 🩸 A secret is a path, never a value. The only identifier this binary logs is
|
|
||||||
//! the store path; see `homeserver`'s module doc for the same rule applied to
|
|
||||||
//! error messages.
|
|
||||||
|
|
||||||
mod homeserver;
|
|
||||||
mod registration;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use swarm_secret_client::{
|
use swarm_secret_client::{
|
||||||
|
|
@ -28,25 +19,27 @@ use swarm_secret_client::{
|
||||||
matrix,
|
matrix,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::{homeserver, registration};
|
||||||
|
|
||||||
/// Role on the store's `cert` auth mount to log in with. Its policy is what
|
/// Role on the store's `cert` auth mount to log in with. Its policy is what
|
||||||
/// allows the write below; the certificate the `BAO_*` variables name has to
|
/// allows the write below; the certificate the `BAO_*` variables name has to
|
||||||
/// carry the CN that role accepts.
|
/// carry the CN that role accepts.
|
||||||
const ENV_CERT_ROLE: &str = "MATRIX_MINTER_CERT_ROLE";
|
const ENV_CERT_ROLE: &str = "MATRIX_MINT_CERT_ROLE";
|
||||||
/// Client-server API base of the homeserver beside us — loopback, since the
|
/// Client-server API base of the homeserver beside us — loopback, since the
|
||||||
/// container shares the host netns.
|
/// container shares the host netns.
|
||||||
const ENV_API_URL: &str = "MATRIX_MINTER_API_URL";
|
const ENV_API_URL: &str = "MATRIX_MINT_API_URL";
|
||||||
/// The bind-mounted appservice registration, which is where the `as_token`
|
/// The bind-mounted appservice registration, which is where the `as_token`
|
||||||
/// comes from. A path, never a value.
|
/// comes from. A path, never a value.
|
||||||
const ENV_REGISTRATION: &str = "MATRIX_MINTER_REGISTRATION";
|
const ENV_REGISTRATION: &str = "MATRIX_MINT_REGISTRATION";
|
||||||
/// Localpart of the hive's account. The appservice registration's own
|
/// Localpart of the appservice's sender account. The registration's own
|
||||||
/// `sender_localpart`, and `hive-c0re`'s `matrix::HIVE_LOCALPART`.
|
/// `sender_localpart`, and `hive-c0re`'s `matrix::HIVE_LOCALPART`.
|
||||||
const ENV_LOCALPART: &str = "MATRIX_MINTER_LOCALPART";
|
const ENV_LOCALPART: &str = "MATRIX_MINT_LOCALPART";
|
||||||
/// Public base URL of the homeserver, stored beside the token so a reader can
|
/// Public base URL of the homeserver, stored beside the token so a reader can
|
||||||
/// reconstruct where it is good for. Optional: a swarm with no gateway vhost
|
/// reconstruct where it is good for. Optional: a swarm with no gateway vhost
|
||||||
/// has no such URL, and `matrix::Credential` types the field to say so.
|
/// has no such URL, and `matrix::Credential` types the field to say so.
|
||||||
const ENV_HOMESERVER: &str = "MATRIX_MINTER_HOMESERVER";
|
const ENV_HOMESERVER: &str = "MATRIX_MINT_HOMESERVER";
|
||||||
|
|
||||||
/// Everything the unit tells this process, checked before anything is opened.
|
/// Everything the unit tells this verb, checked before anything is opened.
|
||||||
///
|
///
|
||||||
/// Separate from the work for the reason `swarm_secret_client::client::Settings`
|
/// Separate from the work for the reason `swarm_secret_client::client::Settings`
|
||||||
/// is: every arm is a misconfiguration an operator reads an error about, and
|
/// is: every arm is a misconfiguration an operator reads an error about, and
|
||||||
|
|
@ -61,7 +54,7 @@ struct Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
/// Read the `MATRIX_MINTER_*` variables from the process environment.
|
/// Read the `MATRIX_MINT_*` variables from the process environment.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Naming the first variable that is unset or empty.
|
/// Naming the first variable that is unset or empty.
|
||||||
|
|
@ -113,15 +106,13 @@ async fn already_published(store: &SecretStore, path: &str) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
/// Run the verb.
|
||||||
async fn main() -> Result<()> {
|
///
|
||||||
tracing_subscriber::fmt()
|
/// # Errors
|
||||||
.with_env_filter(
|
/// If the environment is incomplete, the store refuses the login or the write,
|
||||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
/// the registration cannot be read, or the homeserver refuses both the
|
||||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
/// registration and the appservice login.
|
||||||
)
|
pub async fn run() -> Result<()> {
|
||||||
.init();
|
|
||||||
|
|
||||||
let config = Config::from_env()?;
|
let config = Config::from_env()?;
|
||||||
// Explicitly, rather than through `SecretStore::from_env`: a missing or
|
// Explicitly, rather than through `SecretStore::from_env`: a missing or
|
||||||
// misspelled `BAO_*` variable is the most likely thing to be wrong with a
|
// misspelled `BAO_*` variable is the most likely thing to be wrong with a
|
||||||
|
|
@ -152,7 +143,7 @@ async fn main() -> Result<()> {
|
||||||
// The expected arm, not an edge case: this account is the
|
// The expected arm, not an edge case: this account is the
|
||||||
// appservice's own `sender_localpart`, so the homeserver creates it
|
// appservice's own `sender_localpart`, so the homeserver creates it
|
||||||
// when it loads the registration — before anything gets to ask.
|
// when it loads the registration — before anything gets to ask.
|
||||||
tracing::info!("the @hive: account exists; logging in as the appservice instead");
|
tracing::info!("the sender account exists; logging in as the appservice instead");
|
||||||
homeserver::appservice_login(&http, &config.api_url, &config.localpart, &as_token)
|
homeserver::appservice_login(&http, &config.api_url, &config.localpart, &as_token)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
|
|
@ -179,7 +170,7 @@ mod tests {
|
||||||
/// A lookup standing in for a fully-configured unit's environment.
|
/// A lookup standing in for a fully-configured unit's environment.
|
||||||
fn full(k: &str) -> Option<String> {
|
fn full(k: &str) -> Option<String> {
|
||||||
match k {
|
match k {
|
||||||
ENV_CERT_ROLE => Some("swarm-matrix-minter".to_owned()),
|
ENV_CERT_ROLE => Some("swarm-matrix-ctl".to_owned()),
|
||||||
ENV_API_URL => Some("http://127.0.0.1:8008".to_owned()),
|
ENV_API_URL => Some("http://127.0.0.1:8008".to_owned()),
|
||||||
ENV_REGISTRATION => {
|
ENV_REGISTRATION => {
|
||||||
Some("/var/lib/hyperhive/matrix-appservice/hyperhive.yaml".to_owned())
|
Some("/var/lib/hyperhive/matrix-appservice/hyperhive.yaml".to_owned())
|
||||||
|
|
@ -235,6 +226,26 @@ mod tests {
|
||||||
assert_eq!(c.homeserver, None);
|
assert_eq!(c.homeserver, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The environment prefix is a contract with the nix unit, and the crate
|
||||||
|
/// rename that produced it moved every one of these. A verb-scoped prefix
|
||||||
|
/// is the point: the next verb brings its own, instead of widening a
|
||||||
|
/// binary-scoped one nobody can then narrow.
|
||||||
|
#[test]
|
||||||
|
fn every_variable_is_scoped_to_the_verb() {
|
||||||
|
for var in [
|
||||||
|
ENV_CERT_ROLE,
|
||||||
|
ENV_API_URL,
|
||||||
|
ENV_REGISTRATION,
|
||||||
|
ENV_LOCALPART,
|
||||||
|
ENV_HOMESERVER,
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
var.starts_with("MATRIX_MINT_"),
|
||||||
|
"{var} is not scoped to the mint verb"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_published_path_is_the_one_the_hive_reads() {
|
fn the_published_path_is_the_one_the_hive_reads() {
|
||||||
// Both ends of this slice's loop resolve the same function, so there is
|
// Both ends of this slice's loop resolve the same function, so there is
|
||||||
Loading…
Reference in a new issue