the operator's bao works on a store host without a hive
This commit is contained in:
parent
039896c506
commit
32155f70aa
5 changed files with 169 additions and 9 deletions
|
|
@ -133,6 +133,11 @@ in
|
|||
servicesIssuerClientCertFile = lib.mkDefault "${pkiDir}/services-issuer.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
|
||||
# it. ⚠️ Unlike every other leaf here, never the file an operator copies:
|
||||
# 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 \
|
||||
${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 ''
|
||||
# This hive's gateway-leaf identity, which opens
|
||||
# `pki/issue/<hiveLeafPkiRolePrefix>-<hive>` and nothing else. Its
|
||||
|
|
|
|||
|
|
@ -1590,19 +1590,32 @@ let
|
|||
# same class of surprise it exists to remove.
|
||||
baoCli =
|
||||
let
|
||||
# The hive reader identity `glue-bao-tls` mints where a hive runs, so
|
||||
# cert-auth login needs nothing typed either. A store host without a hive
|
||||
# has no such identity and the operator supplies a token or a leaf; a
|
||||
# wrapper cannot conjure either.
|
||||
# The hive reader identity `glue-bao-tls` mints where a hive runs; a
|
||||
# store host without a hive presents the operator leaf instead. Either
|
||||
# satisfies the listeners' client-certificate requirement and neither
|
||||
# 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 = [
|
||||
"--set-default BAO_ADDR ${lib.escapeShellArg "https://${cfg.domain}:${toString cfg.port}"}"
|
||||
]
|
||||
++ lib.optional (
|
||||
baoDeploy.serverCaFile != null
|
||||
) "--set-default BAO_CACERT ${lib.escapeShellArg baoDeploy.serverCaFile}"
|
||||
++ lib.optionals (baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null) [
|
||||
"--set-default BAO_CLIENT_CERT ${lib.escapeShellArg baoDeploy.clientCertFile}"
|
||||
"--set-default BAO_CLIENT_KEY ${lib.escapeShellArg baoDeploy.clientKeyFile}"
|
||||
++ lib.optionals (identity != null) [
|
||||
"--set-default BAO_CLIENT_CERT ${lib.escapeShellArg identity.cert}"
|
||||
"--set-default BAO_CLIENT_KEY ${lib.escapeShellArg identity.key}"
|
||||
];
|
||||
in
|
||||
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 {
|
||||
type = lib.types.str;
|
||||
default = "bao-granter";
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ let
|
|||
deployCfg.bao.servicesIssuerCommonName
|
||||
deployCfg.bao.natsCommonName
|
||||
deployCfg.bao.granterCommonName
|
||||
deployCfg.bao.operatorCommonName
|
||||
deployCfg.bao.natsAuthCommonName
|
||||
deployCfg.bao.ciRunnerCommonName
|
||||
deployCfg.bao.logsAuthCommonName
|
||||
|
|
|
|||
|
|
@ -98,6 +98,16 @@ let
|
|||
|
||||
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 (
|
||||
p: (p.name or "") == "swarmctl-configured"
|
||||
) 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
|
||||
&& 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";
|
||||
ok = lib.hasInfix "--set-default BAO_ADDR" baoWrapperCmd;
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ let
|
|||
forwarderOidcCommonName = "fwctl";
|
||||
forgePublisherCommonName = "fpctl";
|
||||
granterCommonName = "grctl";
|
||||
operatorCommonName = "opctl";
|
||||
hiveLeafCommonNamePrefix = "hl";
|
||||
};
|
||||
swarm.hives = {
|
||||
|
|
@ -71,6 +72,7 @@ let
|
|||
fwctl.domain = "f.t.local";
|
||||
fpctl.domain = "fp.t.local";
|
||||
grctl.domain = "gr.t.local";
|
||||
opctl.domain = "op.t.local";
|
||||
other.domain = "other.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";
|
||||
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 role is `<prefix>-<hive>`, so the reserved string has to be composed
|
||||
|
|
|
|||
Loading…
Reference in a new issue