the operator's bao works on a store host without a hive
This commit is contained in:
parent
039896c506
commit
32155f70aa
9 changed files with 169 additions and 9 deletions
|
|
@ -99,7 +99,10 @@ and the root token **once**, and stores none of them: not in a file, and the roo
|
||||||
never in a process's argument list (so not in `ps`). Record them off this host before you do
|
never in a process's argument list (so not in `ps`). Record them off this host before you do
|
||||||
anything else. It refuses an initialised store, and works
|
anything else. It refuses an initialised store, and works
|
||||||
from any host that can reach the store and has `bao` on `PATH`; the
|
from any host that can reach the store and has `bao` on `PATH`; the
|
||||||
wrapper installed on the store host already knows the store's address and CA.
|
wrapper installed on the store host already knows the store's address and CA, and
|
||||||
|
presents a client certificate: the hive's own where a hive runs on that host, else
|
||||||
|
the `operator` leaf that `swarm-bao-pki` mints. Both open the TLS handshake, and
|
||||||
|
neither logs in to a role.
|
||||||
|
|
||||||
Then bootstrap the **granter**, the one principal that writes every
|
Then bootstrap the **granter**, the one principal that writes every
|
||||||
`swarm-*` policy and role from then on. This is the only step that needs
|
`swarm-*` policy and role from then on. This is the only step that needs
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,13 @@ over the VPN (for a hive, `ca.pem` and the files from
|
||||||
`/var/lib/swarm-bao-pki/hives/<name>/`), then deploy the other machines. The
|
`/var/lib/swarm-bao-pki/hives/<name>/`), then deploy the other machines. The
|
||||||
private keys travel over a channel you trust and stay readable by root only.
|
private keys travel over a channel you trust and stay readable by root only.
|
||||||
[`credentials.md`](credentials.md) lists which leaf each service reads and what
|
[`credentials.md`](credentials.md) lists which leaf each service reads and what
|
||||||
a missing one costs. The first-run steps after the deploys are in
|
a missing one costs.
|
||||||
|
|
||||||
|
On `swarm-central` itself, `bao status`, `bao operator init` and `bao operator
|
||||||
|
unseal` work as root with no extra step. `swarm-central` runs no hive, so the
|
||||||
|
`bao` wrapper presents the `operator` leaf that the store's host mints for
|
||||||
|
itself. That leaf logs in to no role, so every other call still needs a token.
|
||||||
|
The first-run steps after the deploys are in
|
||||||
[`setup.md`](../../1-getting-started/setup.md).
|
[`setup.md`](../../1-getting-started/setup.md).
|
||||||
|
|
||||||
## DNS
|
## DNS
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,14 @@ store's host signs them and the operator copies them to the host that needs
|
||||||
them and names them in config. The service principals' leaves are at the top of
|
them and names them in config. The service principals' leaves are at the top of
|
||||||
`/var/lib/swarm-bao-pki/`, named after the principal.
|
`/var/lib/swarm-bao-pki/`, named after the principal.
|
||||||
|
|
||||||
|
The host's `bao` wrapper presents the hive's own `client.pem` where a hive runs
|
||||||
|
on the host, and otherwise `operator.pem`, which `swarm-bao-pki` mints on every
|
||||||
|
store host (subject `deploy.bao.operatorCommonName`, `operator` by default; key
|
||||||
|
`0600` root). No cert-auth role accepts that subject, so it opens the TLS
|
||||||
|
handshake and logs in to nothing: `bao status`, `operator init` and `operator
|
||||||
|
unseal` need no token and work on a store host without a hive, and every other
|
||||||
|
call still needs a token. Don't copy it to another host.
|
||||||
|
|
||||||
For every hive in `swarm.hives` other than its own, the store's host signs
|
For every hive in `swarm.hives` other than its own, the store's host signs
|
||||||
that hive's two leaves into `/var/lib/swarm-bao-pki/hives/<hive>/`:
|
that hive's two leaves into `/var/lib/swarm-bao-pki/hives/<hive>/`:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ pieces fit, and the reasoning behind them, is in [`credentials.md`](../../2-hand
|
||||||
| the swarm controller's webhook secret | `swarm-controller` stores it in the swarm secret store at `swarm/controller/swarm-controller/forge/webhook-secret` when the store holds none: the value of `webhook-secret` in its state directory, or a new one when there is no such file. The state directory file stays on disk and is the fallback while the store is unreachable or its policy doesn't grant the path yet | the swarm secret store; the controller holds it in memory and registers the same value with the forge's hooks | nothing to place: the controller's store leaf already exists. A store that stays unreachable on a start with no state directory file leaves the webhook endpoint answering 503 until the store answers: the controller re-reads it in the background, at most five minutes apart, and registers the forge hooks once it has the secret |
|
| the swarm controller's webhook secret | `swarm-controller` stores it in the swarm secret store at `swarm/controller/swarm-controller/forge/webhook-secret` when the store holds none: the value of `webhook-secret` in its state directory, or a new one when there is no such file. The state directory file stays on disk and is the fallback while the store is unreachable or its policy doesn't grant the path yet | the swarm secret store; the controller holds it in memory and registers the same value with the forge's hooks | nothing to place: the controller's store leaf already exists. A store that stays unreachable on a start with no state directory file leaves the webhook endpoint answering 503 until the store answers: the controller re-reads it in the background, at most five minutes apart, and registers the forge hooks once it has the secret |
|
||||||
| the metrics store responder's own store leaf (cert + key) | `signLeaf` in `glue-bao-tls.nix` (`swarm-bao-pki.service`), on the host that runs the secret store, once, subject `deploy.bao.metricsAuthCommonName`. `swarm-bao-metrics-auth-policy.service` writes the `swarm-metrics-auth` role and its read grants on the bearer leaf and on the metrics store forwarder's client secret, nothing else | `/var/lib/swarm-bao-pki/metrics-auth.pem` and `metrics-auth-key.pem` on the store's host, handed to `swarm-metrics-auth` through `LoadCredential` (paths in argv, never values) and read by path by `swarm-victoriametrics-forwarder-oidc` | **the operator** places a leaf with that subject on the metrics store's host and names it in `deploy.victoriametrics.authBaoClientCertFile` / `authBaoClientKeyFile`; evaluation fails naming both until then. If the store is on another host, deploy that host first: the role must exist before the responder's login succeeds, and until it does the gateway refuses every Grafana query (fail closed) |
|
| the metrics store responder's own store leaf (cert + key) | `signLeaf` in `glue-bao-tls.nix` (`swarm-bao-pki.service`), on the host that runs the secret store, once, subject `deploy.bao.metricsAuthCommonName`. `swarm-bao-metrics-auth-policy.service` writes the `swarm-metrics-auth` role and its read grants on the bearer leaf and on the metrics store forwarder's client secret, nothing else | `/var/lib/swarm-bao-pki/metrics-auth.pem` and `metrics-auth-key.pem` on the store's host, handed to `swarm-metrics-auth` through `LoadCredential` (paths in argv, never values) and read by path by `swarm-victoriametrics-forwarder-oidc` | **the operator** places a leaf with that subject on the metrics store's host and names it in `deploy.victoriametrics.authBaoClientCertFile` / `authBaoClientKeyFile`; evaluation fails naming both until then. If the store is on another host, deploy that host first: the role must exist before the responder's login succeeds, and until it does the gateway refuses every Grafana query (fail closed) |
|
||||||
| the secret publisher's store leaf (cert + key) | `signLeaf` in `glue-bao-tls.nix` (`swarm-bao-pki.service`), on the host that runs the secret store, once, subject `deploy.bao.secretPublisherCommonName`. `swarm-bao-secret-publisher-policy.service` writes the `swarm-secret-publisher` role: create and update under `swarm/hives/*`, `swarm/services/*` and the controller's client, and read on the `oidc/client` leaves it mints, the controller's client and authelia's five keys alone | `/var/lib/swarm-bao-pki/secret-publisher.pem` and `secret-publisher-key.pem` on the host, handed into authelia's container as nspawn system credentials, which `authelia-swarm-secrets`, `swarm-authelia-bridge` and the forwarder's `swarm-journal-forwarder-oidc` load with `LoadCredential`. nspawn won't start authelia's container while a named file is missing | **the operator** copies the leaf to authelia's host, beside the hive's own store leaf, where `glue-secret-publisher-bao-identity.nix` looks, or names it in `deploy.swarm-secret-publisher.baoClientCertFile` / `baoClientKeyFile` |
|
| the secret publisher's store leaf (cert + key) | `signLeaf` in `glue-bao-tls.nix` (`swarm-bao-pki.service`), on the host that runs the secret store, once, subject `deploy.bao.secretPublisherCommonName`. `swarm-bao-secret-publisher-policy.service` writes the `swarm-secret-publisher` role: create and update under `swarm/hives/*`, `swarm/services/*` and the controller's client, and read on the `oidc/client` leaves it mints, the controller's client and authelia's five keys alone | `/var/lib/swarm-bao-pki/secret-publisher.pem` and `secret-publisher-key.pem` on the host, handed into authelia's container as nspawn system credentials, which `authelia-swarm-secrets`, `swarm-authelia-bridge` and the forwarder's `swarm-journal-forwarder-oidc` load with `LoadCredential`. nspawn won't start authelia's container while a named file is missing | **the operator** copies the leaf to authelia's host, beside the hive's own store leaf, where `glue-secret-publisher-bao-identity.nix` looks, or names it in `deploy.swarm-secret-publisher.baoClientCertFile` / `baoClientKeyFile` |
|
||||||
|
| the operator's store leaf (cert + key) | `signLeaf` in `glue-bao-tls.nix` (`swarm-bao-pki.service`), on every host that runs the secret store, once, subject `deploy.bao.operatorCommonName`. No cert-auth role accepts that subject, so the leaf opens the TLS handshake and logs in to nothing | `/var/lib/swarm-bao-pki/operator.pem` and `operator-key.pem` on the host, `0600` root; the host's `bao` wrapper presents it when the host runs no hive, via `deploy.bao.operatorClientCertFile` / `operatorClientKeyFile`. A store host from before this leaf has no file until `swarm-bao-pki` next runs, and its wrapper sets no client certificate | nothing to place: the leaf is for the shell on the store's host and is never copied to another host |
|
||||||
| the swarm controller's copy of its OIDC secret | `swarm-controller` reads it out of the swarm secret store at start, under its own leaf | the daemon's memory only; never on disk | same read, co-located or not. Nothing for an operator to place beyond the controller's store leaf (`deploy.swarm-controller.baoClientCertFile`) |
|
| the swarm controller's copy of its OIDC secret | `swarm-controller` reads it out of the swarm secret store at start, under its own leaf | the daemon's memory only; never on disk | same read, co-located or not. Nothing for an operator to place beyond the controller's store leaf (`deploy.swarm-controller.baoClientCertFile`) |
|
||||||
| a hive's copy of its own OIDC secret | hive-c0re reads it out of the swarm secret store when it first connects to the queue, under the hive's own leaf | the daemon's memory only; never on disk | same read, co-located or not. Nothing for an operator to place beyond the hive's store leaf (`deploy.bao.clientCertFile`) |
|
| a hive's copy of its own OIDC secret | hive-c0re reads it out of the swarm secret store when it first connects to the queue, under the hive's own leaf | the daemon's memory only; never on disk | same read, co-located or not. Nothing for an operator to place beyond the hive's store leaf (`deploy.bao.clientCertFile`) |
|
||||||
| the hive collector's copy of its hive's OIDC secret | `swarm-bao-hive-otel-oidc.service` reads it out of the swarm secret store under the hive's own leaf, **on every hive that runs the collector and holds that leaf**; the leaf is the source | `/run/hive-otel-oidc/hive-<name>.secret` on the hive's host tmpfs, 0400 root, loaded by `LoadCredential` — never on persistent disk, so a reboot fetches it again | same unit, same path, co-located or not. Nothing for an operator to place beyond the hive's store leaf (`deploy.bao.clientCertFile`). Naming a secret file with `otel.clientSecretFile` instead is legacy for a host without a leaf: the build warns, and the fix is to place the leaf from `/var/lib/swarm-bao-pki/hives/<hive>/` and drop the setting |
|
| the hive collector's copy of its hive's OIDC secret | `swarm-bao-hive-otel-oidc.service` reads it out of the swarm secret store under the hive's own leaf, **on every hive that runs the collector and holds that leaf**; the leaf is the source | `/run/hive-otel-oidc/hive-<name>.secret` on the hive's host tmpfs, 0400 root, loaded by `LoadCredential` — never on persistent disk, so a reboot fetches it again | same unit, same path, co-located or not. Nothing for an operator to place beyond the hive's store leaf (`deploy.bao.clientCertFile`). Naming a secret file with `otel.clientSecretFile` instead is legacy for a host without a leaf: the build warns, and the fix is to place the leaf from `/var/lib/swarm-bao-pki/hives/<hive>/` and drop the setting |
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,11 @@ in
|
||||||
servicesIssuerClientCertFile = lib.mkDefault "${pkiDir}/services-issuer.pem";
|
servicesIssuerClientCertFile = lib.mkDefault "${pkiDir}/services-issuer.pem";
|
||||||
servicesIssuerClientKeyFile = lib.mkDefault "${pkiDir}/services-issuer-key.pem";
|
servicesIssuerClientKeyFile = lib.mkDefault "${pkiDir}/services-issuer-key.pem";
|
||||||
|
|
||||||
|
# The operator's shell on a store host that runs no hive, which has no
|
||||||
|
# reader above. See ./swarm-bao.nix's `baoCli`.
|
||||||
|
operatorClientCertFile = lib.mkDefault "${pkiDir}/operator.pem";
|
||||||
|
operatorClientKeyFile = lib.mkDefault "${pkiDir}/operator-key.pem";
|
||||||
|
|
||||||
# The granter: every `swarm-bao-*-policy` unit on this host logs in with
|
# The granter: every `swarm-bao-*-policy` unit on this host logs in with
|
||||||
# it. ⚠️ Unlike every other leaf here, never the file an operator copies:
|
# it. ⚠️ Unlike every other leaf here, never the file an operator copies:
|
||||||
# its policy is root-equivalent and its only reader is this host.
|
# its policy is root-equivalent and its only reader is this host.
|
||||||
|
|
@ -352,6 +357,13 @@ in
|
||||||
[ -s ${pkiDir}/forge-publisher.pem ] || ${signLeaf} ${pkiDir} forge-publisher \
|
[ -s ${pkiDir}/forge-publisher.pem ] || ${signLeaf} ${pkiDir} forge-publisher \
|
||||||
${lib.escapeShellArg deployCfg.bao.forgePublisherCommonName} "" clientAuth
|
${lib.escapeShellArg deployCfg.bao.forgePublisherCommonName} "" clientAuth
|
||||||
|
|
||||||
|
# The operator's shell on this host. No cert-auth role accepts its
|
||||||
|
# subject, so it opens the TLS handshake and nothing behind it. Minted
|
||||||
|
# whether or not a hive runs; the `bao` wrapper presents it only when
|
||||||
|
# none does.
|
||||||
|
[ -s ${pkiDir}/operator.pem ] || ${signLeaf} ${pkiDir} operator \
|
||||||
|
${lib.escapeShellArg deployCfg.bao.operatorCommonName} "" clientAuth
|
||||||
|
|
||||||
${lib.optionalString runsHive ''
|
${lib.optionalString runsHive ''
|
||||||
# This hive's gateway-leaf identity, which opens
|
# This hive's gateway-leaf identity, which opens
|
||||||
# `pki/issue/<hiveLeafPkiRolePrefix>-<hive>` and nothing else. Its
|
# `pki/issue/<hiveLeafPkiRolePrefix>-<hive>` and nothing else. Its
|
||||||
|
|
|
||||||
|
|
@ -1590,19 +1590,32 @@ let
|
||||||
# same class of surprise it exists to remove.
|
# same class of surprise it exists to remove.
|
||||||
baoCli =
|
baoCli =
|
||||||
let
|
let
|
||||||
# The hive reader identity `glue-bao-tls` mints where a hive runs, so
|
# The hive reader identity `glue-bao-tls` mints where a hive runs; a
|
||||||
# cert-auth login needs nothing typed either. A store host without a hive
|
# store host without a hive presents the operator leaf instead. Either
|
||||||
# has no such identity and the operator supplies a token or a leaf; a
|
# satisfies the listeners' client-certificate requirement and neither
|
||||||
# wrapper cannot conjure either.
|
# supplies a token: a wrapper cannot conjure one.
|
||||||
|
identity =
|
||||||
|
if baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null then
|
||||||
|
{
|
||||||
|
cert = baoDeploy.clientCertFile;
|
||||||
|
key = baoDeploy.clientKeyFile;
|
||||||
|
}
|
||||||
|
else if baoDeploy.operatorClientCertFile != null && baoDeploy.operatorClientKeyFile != null then
|
||||||
|
{
|
||||||
|
cert = baoDeploy.operatorClientCertFile;
|
||||||
|
key = baoDeploy.operatorClientKeyFile;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
null;
|
||||||
wrapArgs = [
|
wrapArgs = [
|
||||||
"--set-default BAO_ADDR ${lib.escapeShellArg "https://${cfg.domain}:${toString cfg.port}"}"
|
"--set-default BAO_ADDR ${lib.escapeShellArg "https://${cfg.domain}:${toString cfg.port}"}"
|
||||||
]
|
]
|
||||||
++ lib.optional (
|
++ lib.optional (
|
||||||
baoDeploy.serverCaFile != null
|
baoDeploy.serverCaFile != null
|
||||||
) "--set-default BAO_CACERT ${lib.escapeShellArg baoDeploy.serverCaFile}"
|
) "--set-default BAO_CACERT ${lib.escapeShellArg baoDeploy.serverCaFile}"
|
||||||
++ lib.optionals (baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null) [
|
++ lib.optionals (identity != null) [
|
||||||
"--set-default BAO_CLIENT_CERT ${lib.escapeShellArg baoDeploy.clientCertFile}"
|
"--set-default BAO_CLIENT_CERT ${lib.escapeShellArg identity.cert}"
|
||||||
"--set-default BAO_CLIENT_KEY ${lib.escapeShellArg baoDeploy.clientKeyFile}"
|
"--set-default BAO_CLIENT_KEY ${lib.escapeShellArg identity.key}"
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
pkgs.symlinkJoin {
|
pkgs.symlinkJoin {
|
||||||
|
|
@ -2859,6 +2872,47 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
operatorCommonName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "operator";
|
||||||
|
description = ''
|
||||||
|
Subject of the leaf the store's host mints for the operator's `bao`
|
||||||
|
shell. It authenticates the TLS handshake and nothing else: no
|
||||||
|
cert-auth role accepts this subject, so a login with it is refused and
|
||||||
|
it holds no token. Bao answers `init`, `unseal` and `status` without a
|
||||||
|
token; every other call still needs one.
|
||||||
|
|
||||||
|
⚠️ Reserved as a hive name by ./swarm.nix: a hive of this name has a reader
|
||||||
|
role that accepts this subject, so the operator's leaf would log in as it.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
operatorClientCertFile = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/var/lib/swarm-bao-pki/operator.pem";
|
||||||
|
description = ''
|
||||||
|
Certificate the host's `bao` wrapper presents when the host has no
|
||||||
|
{option}`services.hyperhive.deploy.bao.clientCertFile`, which is a
|
||||||
|
store host that runs no hive. Its subject must be
|
||||||
|
{option}`services.hyperhive.deploy.bao.operatorCommonName`.
|
||||||
|
|
||||||
|
./glue-bao-tls.nix points it at the leaf it mints on the store's host.
|
||||||
|
A path, never a value.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
operatorClientKeyFile = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/var/lib/swarm-bao-pki/operator-key.pem";
|
||||||
|
description = ''
|
||||||
|
Private key for
|
||||||
|
{option}`services.hyperhive.deploy.bao.operatorClientCertFile`. A path,
|
||||||
|
never a value.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
granterCommonName = lib.mkOption {
|
granterCommonName = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "bao-granter";
|
default = "bao-granter";
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ let
|
||||||
deployCfg.bao.servicesIssuerCommonName
|
deployCfg.bao.servicesIssuerCommonName
|
||||||
deployCfg.bao.natsCommonName
|
deployCfg.bao.natsCommonName
|
||||||
deployCfg.bao.granterCommonName
|
deployCfg.bao.granterCommonName
|
||||||
|
deployCfg.bao.operatorCommonName
|
||||||
deployCfg.bao.natsAuthCommonName
|
deployCfg.bao.natsAuthCommonName
|
||||||
deployCfg.bao.ciRunnerCommonName
|
deployCfg.bao.ciRunnerCommonName
|
||||||
deployCfg.bao.logsAuthCommonName
|
deployCfg.bao.logsAuthCommonName
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,16 @@ let
|
||||||
|
|
||||||
baoWrapperCmd = if baoWrapper == null then "" else (baoWrapper.buildCommand or "");
|
baoWrapperCmd = if baoWrapper == null then "" else (baoWrapper.buildCommand or "");
|
||||||
|
|
||||||
|
# The wrapper's rendered command on any store host.
|
||||||
|
wrapperCmdOf =
|
||||||
|
m:
|
||||||
|
let
|
||||||
|
w = lib.findFirst (p: (p.name or "") == "bao-hive") null m.environment.systemPackages;
|
||||||
|
in
|
||||||
|
if w == null then "" else (w.buildCommand or "");
|
||||||
|
|
||||||
|
pkiDir = import ../host-modules/lib/bao-pki-dir.nix;
|
||||||
|
|
||||||
swarmctlWrapper = lib.findFirst (
|
swarmctlWrapper = lib.findFirst (
|
||||||
p: (p.name or "") == "swarmctl-configured"
|
p: (p.name or "") == "swarmctl-configured"
|
||||||
) null controllerNoStore.environment.systemPackages;
|
) null controllerNoStore.environment.systemPackages;
|
||||||
|
|
@ -285,6 +295,63 @@ let
|
||||||
&& !builtins.elem "hive-client-ca.pem:/etc/pki/store-server-ca.pem" s.swarm-controller.serviceConfig.LoadCredential
|
&& !builtins.elem "hive-client-ca.pem:/etc/pki/store-server-ca.pem" s.swarm-controller.serviceConfig.LoadCredential
|
||||||
&& m.deploy.swarm-controller.hiveClientCaFile == m.deploy.bao.clientCaFile;
|
&& m.deploy.swarm-controller.hiveClientCaFile == m.deploy.bao.clientCaFile;
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
# A store host without a hive has no reader leaf, so the wrapper presents
|
||||||
|
# the operator leaf that the PKI unit mints. `BAO_ADDR` in the same
|
||||||
|
# command is the control that the wrapper was found at all.
|
||||||
|
name = "the wrapped bao CLI on a store host without a hive presents the operator leaf";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
cmd = wrapperCmdOf baoControllerHere;
|
||||||
|
in
|
||||||
|
!baoControllerHere.services.hyperhive.deploy.hive-controller.enable
|
||||||
|
&& lib.hasInfix "--set-default BAO_ADDR" cmd
|
||||||
|
&& lib.hasInfix "--set-default BAO_CLIENT_CERT ${lib.escapeShellArg "${pkiDir}/operator.pem"}" cmd
|
||||||
|
&& lib.hasInfix "--set-default BAO_CLIENT_KEY ${lib.escapeShellArg "${pkiDir}/operator-key.pem"}" cmd
|
||||||
|
&& lib.hasInfix "${pkiDir}/operator.pem ] || " baoControllerHere.systemd.services.swarm-bao-pki.script;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
# A host with a hive keeps its reader leaf in the wrapper and does not
|
||||||
|
# name the operator's. The reader leaf in the same command is the
|
||||||
|
# presence control.
|
||||||
|
name = "the wrapped bao CLI on a store host with a hive keeps the hive's leaf";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
cmd = wrapperCmdOf shared.allLocal;
|
||||||
|
in
|
||||||
|
shared.allLocal.services.hyperhive.deploy.hive-controller.enable
|
||||||
|
&& lib.hasInfix "--set-default BAO_CLIENT_CERT ${lib.escapeShellArg "${pkiDir}/client.pem"}" cmd
|
||||||
|
&& !lib.hasInfix "operator" cmd;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
# The operator leaf exists only where the store runs. The store host's
|
||||||
|
# PKI unit above is the presence control.
|
||||||
|
name = "a host that does not run the store has no operator leaf";
|
||||||
|
ok =
|
||||||
|
!(controllerNoStore.systemd.services ? swarm-bao-pki)
|
||||||
|
&& controllerNoStore.services.hyperhive.deploy.bao.operatorClientCertFile == null
|
||||||
|
&& controllerNoStore.services.hyperhive.deploy.bao.operatorClientKeyFile == null;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
# No cert-auth role accepts the operator leaf's subject, so the leaf
|
||||||
|
# opens the TLS handshake and logs in to nothing. Every role a unit
|
||||||
|
# writes names its subject in `allowed_common_names`; the control is that
|
||||||
|
# the roles are found and include the controller's.
|
||||||
|
name = "no cert-auth role the store host writes accepts the operator leaf's subject";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
scripts = lib.concatMapStringsSep "\n" (u: u.script or "") (
|
||||||
|
lib.attrValues baoControllerHere.systemd.services
|
||||||
|
);
|
||||||
|
subjects = map lib.head (
|
||||||
|
builtins.filter lib.isList (builtins.split "allowed_common_names=([^ \\\n]+)" scripts)
|
||||||
|
);
|
||||||
|
operatorCn = baoControllerHere.services.hyperhive.deploy.bao.operatorCommonName;
|
||||||
|
in
|
||||||
|
lib.elem baoControllerHere.services.hyperhive.deploy.bao.controllerCommonName subjects
|
||||||
|
&& lib.length subjects > 5
|
||||||
|
&& !lib.elem operatorCn subjects;
|
||||||
|
}
|
||||||
{
|
{
|
||||||
name = "the wrapped bao CLI carries this store's address";
|
name = "the wrapped bao CLI carries this store's address";
|
||||||
ok = lib.hasInfix "--set-default BAO_ADDR" baoWrapperCmd;
|
ok = lib.hasInfix "--set-default BAO_ADDR" baoWrapperCmd;
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ let
|
||||||
forwarderOidcCommonName = "fwctl";
|
forwarderOidcCommonName = "fwctl";
|
||||||
forgePublisherCommonName = "fpctl";
|
forgePublisherCommonName = "fpctl";
|
||||||
granterCommonName = "grctl";
|
granterCommonName = "grctl";
|
||||||
|
operatorCommonName = "opctl";
|
||||||
hiveLeafCommonNamePrefix = "hl";
|
hiveLeafCommonNamePrefix = "hl";
|
||||||
};
|
};
|
||||||
swarm.hives = {
|
swarm.hives = {
|
||||||
|
|
@ -71,6 +72,7 @@ let
|
||||||
fwctl.domain = "f.t.local";
|
fwctl.domain = "f.t.local";
|
||||||
fpctl.domain = "fp.t.local";
|
fpctl.domain = "fp.t.local";
|
||||||
grctl.domain = "gr.t.local";
|
grctl.domain = "gr.t.local";
|
||||||
|
opctl.domain = "op.t.local";
|
||||||
other.domain = "other.t.local";
|
other.domain = "other.t.local";
|
||||||
hl-other.domain = "hl.t.local";
|
hl-other.domain = "hl.t.local";
|
||||||
};
|
};
|
||||||
|
|
@ -209,6 +211,12 @@ let
|
||||||
name = "a hive named after the bao granter's subject is refused too";
|
name = "a hive named after the bao granter's subject is refused too";
|
||||||
ok = equalityGuardRefuses hiveNamedAfterEverySubject "grctl";
|
ok = equalityGuardRefuses hiveNamedAfterEverySubject "grctl";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
# And the operator's leaf: no role accepts it, but a hive of that name
|
||||||
|
# has a reader role that does.
|
||||||
|
name = "a hive named after the operator leaf's subject is refused too";
|
||||||
|
ok = equalityGuardRefuses hiveNamedAfterEverySubject "opctl";
|
||||||
|
}
|
||||||
{
|
{
|
||||||
# 🩸 The per-hive half, and the one a prefix-only reservation would miss:
|
# 🩸 The per-hive half, and the one a prefix-only reservation would miss:
|
||||||
# the role is `<prefix>-<hive>`, so the reserved string has to be composed
|
# the role is `<prefix>-<hive>`, so the reserved string has to be composed
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue