swarm-bao: write the secret publisher's policy and cert-auth role
A sibling unit rather than more script in swarm-bao-controller-policy, because that unit's name is an operator-facing string: docs/getting-started/setup.md tells a reader to run `systemctl status swarm-bao-controller-policy`. Widening it to two principals makes the name wrong; renaming it makes the instruction wrong. `after` and not `requires`. The controller's unit creates the KV and cert-auth mounts this one writes into, so the ordering is real — but a failed oneshot still counts as finished, so `requires` would neither wait for its success nor re-run this unit when the sibling's own retry eventually lands. Ordering plus this unit's `Restart=on-failure` is what converges. Four module-eval cases, because the unit arrived with every claim about it in prose and the suite still reporting the same count: the grant is write-only and reaches the hive prefix alone (pinned as the whole capability list, since an added capability is what a presence check misses, with negative arms for the agent prefix, the bare swarm prefix and the policy path); it is ordered after the unit that creates the mounts; it renders on the host; and the control, that it does not render inside the store's container. Refs #3853
This commit is contained in:
parent
a597b6bb1c
commit
98f2a94d82
2 changed files with 98 additions and 0 deletions
|
|
@ -208,6 +208,7 @@ let
|
|||
# controller may create policies under that prefix, and a policy it can
|
||||
# rewrite is not a constraint on anything.
|
||||
secretPublisherPolicyName = "swarm-secret-publisher";
|
||||
secretPublisherCn = baoDeploy.secretPublisherCommonName;
|
||||
|
||||
# One grant, and every narrowing in it is load-bearing.
|
||||
#
|
||||
|
|
@ -1006,6 +1007,58 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
# A SIBLING rather than more script in the unit above, because that unit's
|
||||
# name is an operator-facing string: ../../docs/getting-started/setup.md
|
||||
# tells a reader to run `systemctl status swarm-bao-controller-policy`.
|
||||
# Widening it to two principals would make the name wrong, and renaming it
|
||||
# would make that instruction wrong.
|
||||
#
|
||||
# `after` and not `requires`: the unit above creates the KV and cert-auth
|
||||
# mounts this one writes into, but a failed oneshot still counts as
|
||||
# finished, so `requires` would neither wait for its success nor re-run
|
||||
# this one when its own retry eventually lands. Ordering plus this unit's
|
||||
# own retry is what actually converges.
|
||||
systemd.services.swarm-bao-secret-publisher-policy = lib.mkIf haveBootstrapToken {
|
||||
description = "write the swarm secret publisher's bao policy and cert-auth role";
|
||||
after = [
|
||||
"container@${cfg.machine}.service"
|
||||
"swarm-bao-controller-policy.service"
|
||||
];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = [
|
||||
baoCli
|
||||
pkgs.coreutils
|
||||
];
|
||||
unitConfig.ConditionPathExists = baoDeploy.bootstrapTokenFile;
|
||||
# Same unseal wait as its sibling above, for the reason stated there:
|
||||
# under `seal = "shamir"` a human unseals by hand, which can take a day.
|
||||
startLimitBurst = 2880;
|
||||
startLimitIntervalSec = 90000;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
Restart = "on-failure";
|
||||
RestartSec = 30;
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
|
||||
BAO_TOKEN="$(cat ${lib.escapeShellArg baoDeploy.bootstrapTokenFile})"
|
||||
export BAO_TOKEN
|
||||
|
||||
printf '%s' ${lib.escapeShellArg secretPublisherPolicyText} |
|
||||
bao policy write ${lib.escapeShellArg secretPublisherPolicyName} -
|
||||
''
|
||||
+ lib.optionalString (baoDeploy.clientCaFile != null) ''
|
||||
|
||||
bao write auth/cert/certs/${lib.escapeShellArg secretPublisherPolicyName} \
|
||||
certificate=@${tlsDir}/client-ca.pem \
|
||||
allowed_common_names=${lib.escapeShellArg secretPublisherCn} \
|
||||
token_policies=${lib.escapeShellArg secretPublisherPolicyName} \
|
||||
display_name=${lib.escapeShellArg secretPublisherCn}
|
||||
'';
|
||||
};
|
||||
|
||||
containers.${cfg.machine} = {
|
||||
autoStart = true;
|
||||
ephemeral = false;
|
||||
|
|
|
|||
|
|
@ -841,6 +841,51 @@ let
|
|||
in
|
||||
lib.hasInfix "sys/policies/acl/hive-*" s && !(lib.hasInfix "sys/policies/acl/*" s);
|
||||
}
|
||||
{
|
||||
# Same host-side reasoning as the controller's granting unit above: the
|
||||
# write needs a client certificate and the host is the side that has one.
|
||||
name = "a store host with a placed bootstrap token renders the publisher's granting unit too";
|
||||
ok =
|
||||
let
|
||||
u = baoGrantHere.systemd.services.swarm-bao-secret-publisher-policy;
|
||||
in
|
||||
u.unitConfig.ConditionPathExists == "/run/secrets/bao-bootstrap.token"
|
||||
&& lib.hasInfix "swarm-secret-publisher" u.script;
|
||||
}
|
||||
{
|
||||
# The control for the case above, and the same one the controller's unit
|
||||
# has: rendered on the host means NOT rendered in the container, where it
|
||||
# would have neither an identity nor a route to the store.
|
||||
name = "the publisher's granting unit is not rendered inside the store's container";
|
||||
ok =
|
||||
!(baoGrantHere.containers.swarm-bao.config.systemd.services ? swarm-bao-secret-publisher-policy);
|
||||
}
|
||||
{
|
||||
# The whole point of a second principal. `hives/` and not `swarm/`, so it
|
||||
# cannot touch an agent's or a service's credentials; and no `read`, so a
|
||||
# unit whose job is copying a file cannot recover what is already there.
|
||||
# Pinned as the full capability list, because an added capability is
|
||||
# exactly what a presence check misses.
|
||||
name = "the publisher's grant is write-only and reaches the hive prefix alone";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-secret-publisher-policy.script;
|
||||
in
|
||||
lib.hasInfix "path \"secret/data/swarm/hives/*\" {\n capabilities = [\"create\", \"update\"]" s
|
||||
&& !(lib.hasInfix "secret/data/swarm/agents" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/*" s)
|
||||
&& !(lib.hasInfix "sys/policies/acl" s);
|
||||
}
|
||||
{
|
||||
# The ordering is load-bearing and invisible at runtime: the controller's
|
||||
# unit creates the KV and cert-auth mounts this one writes into, so
|
||||
# without it a cold boot races and fails with "route entry not found",
|
||||
# which names neither unit.
|
||||
name = "the publisher's granting unit is ordered after the one that creates the mounts";
|
||||
ok = lib.elem "swarm-bao-controller-policy.service" (
|
||||
baoGrantHere.systemd.services.swarm-bao-secret-publisher-policy.after
|
||||
);
|
||||
}
|
||||
{
|
||||
# The policy authorising this route lives in another file, and nothing
|
||||
# else relates the grants to the paths the code actually writes.
|
||||
|
|
|
|||
Loading…
Reference in a new issue