swarm-bao: the responder may create the callout xkey; every token reads swarm/public/
The swarm-nats-auth role gains create+read on exactly the callout xkey's two leaves, and no update, so a stored pair is never replaced. swarm/public/ is readable by every token through bao's default policy. swarm-bao-public-read-policy reads that policy, replaces only its own marker-delimited block, and writes it back when it differs; it never writes a policy it read back empty. Writing `default` is a new grant on the granter's own policy, so on an existing store it lands only after the bootstrap-token step re-runs; until then the unit says so and exits 0, and swarm-bao-granter-role restarts it on success.
This commit is contained in:
parent
1e0b10bb7d
commit
bb571285fd
3 changed files with 258 additions and 28 deletions
|
|
@ -185,9 +185,11 @@ let
|
|||
# the services root. The next six set up the agent PKI mount: the mount, its
|
||||
# root and the `swarm-*` role agent certificates are issued through. No
|
||||
# `root` delete there: every agent's cert-auth role pins that root by value,
|
||||
# so replacing it would lock every agent out. The last three configure the
|
||||
# so replacing it would lock every agent out. The next three configure the
|
||||
# `oidc` auth method: its config, its `swarm-*` roles, and a read on the one
|
||||
# client secret that config carries. No `sys/auth`: the auth mounts are
|
||||
# client secret that config carries. The last is bao's `default` policy,
|
||||
# which every token carries: `swarm-bao-public-read-policy` adds
|
||||
# the `swarm/public/` read to it. No `sys/auth`: the auth mounts are
|
||||
# created with the bootstrap token by `swarm-bao-granter-role`.
|
||||
#
|
||||
# Piped as a shell-quoted argument like `controllerPolicyText`, so
|
||||
|
|
@ -272,6 +274,10 @@ let
|
|||
path "${credentialMountPath}/data/${baoUiClientLeaf}" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "sys/policies/acl/default" {
|
||||
capabilities = ["create", "read", "update"]
|
||||
}
|
||||
'';
|
||||
|
||||
# What a granting unit prints when the store refuses the granter: the
|
||||
|
|
@ -849,16 +855,49 @@ let
|
|||
# one path segment, where `*` globs only at the end and would reach every
|
||||
# other credential an agent holds. Plus the responder's own OIDC client
|
||||
# secret, which `swarm-bao-nats-auth-oidc` reads under this same leaf.
|
||||
#
|
||||
# And the callout xkey's two leaves, which `ensure-xkey` creates when the
|
||||
# store holds no seed. `create` without `update`, so a stored pair is never
|
||||
# replaced; `read` because creating is read-then-create, and the responder
|
||||
# loads the seed as it starts.
|
||||
natsAuthReaders = [
|
||||
{
|
||||
name = "swarm-nats-auth";
|
||||
cn = baoDeploy.natsAuthCommonName;
|
||||
policyText =
|
||||
readStanza "${credentialMountPath}/data/swarm/agents/+/queue"
|
||||
+ readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.nats.clientId}/oidc/client";
|
||||
+ readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.nats.clientId}/oidc/client"
|
||||
+ createStanza "${credentialMountPath}/data/${xkeySeedLeaf}"
|
||||
+ createStanza "${credentialMountPath}/data/${xkeyPublicLeaf}";
|
||||
}
|
||||
];
|
||||
|
||||
# The callout xkey, the nix half of `swarm-nats-auth`'s `xkey::SEED_PATH`
|
||||
# and `xkey::PUBLIC_PATH`. The seed is the responder's alone; the public half
|
||||
# is under `swarm/public/`, which `publicReadBlock` lets every token read.
|
||||
xkeySeedLeaf = "swarm/services/swarm-nats/callout/xkey-seed";
|
||||
xkeyPublicLeaf = "swarm/public/swarm-nats/callout-xkey";
|
||||
|
||||
createStanza = path: ''
|
||||
path "${path}" {
|
||||
capabilities = ["create", "read"]
|
||||
}
|
||||
'';
|
||||
|
||||
# What `swarm/public/` is: readable by every token the store issues, through
|
||||
# bao's `default` policy, which every token carries. A block between two
|
||||
# marker lines, so `swarm-bao-public-read-policy` can replace it and leave
|
||||
# the rest of that policy, the store's own, as it found it.
|
||||
publicReadBegin = "# hyperhive: swarm/public begin";
|
||||
publicReadEnd = "# hyperhive: swarm/public end";
|
||||
publicReadBlock = lib.concatStringsSep "\n" [
|
||||
publicReadBegin
|
||||
''path "${credentialMountPath}/data/swarm/public/*" {''
|
||||
" capabilities = [\"read\"]"
|
||||
"}"
|
||||
publicReadEnd
|
||||
];
|
||||
|
||||
# The CI runner's token fetch. The one reader of the path, which the
|
||||
# controller writes and cannot read back.
|
||||
ciRunnerReaders = [
|
||||
|
|
@ -2535,12 +2574,12 @@ in
|
|||
Restart = "on-failure";
|
||||
RestartSec = 30;
|
||||
# Runs only once the script above exits 0, which is when the
|
||||
# granter may first configure `auth/oidc`; the viewer unit exits 0
|
||||
# without doing so before then, and nothing else re-runs it. A
|
||||
# restart because it is `RemainAfterExit`, where a start is a
|
||||
# no-op.
|
||||
# granter may first configure `auth/oidc` and write bao's `default`
|
||||
# policy; the viewer and public-read units exit 0 without doing so
|
||||
# before then, and nothing else re-runs them. A restart because
|
||||
# they are `RemainAfterExit`, where a start is a no-op.
|
||||
#
|
||||
# `--no-block`: the viewer unit is ordered after this one, so a
|
||||
# `--no-block`: both units are ordered after this one, so a
|
||||
# blocking restart waits on a job that cannot run until this unit
|
||||
# is active, which it is not until this command returns.
|
||||
#
|
||||
|
|
@ -2549,7 +2588,7 @@ in
|
|||
# already running (or a `systemctl` that itself fails to enqueue
|
||||
# the job) would mark the granter failed and trigger its own
|
||||
# `Restart=on-failure` for a problem that isn't the granter's.
|
||||
ExecStartPost = "-${pkgs.systemd}/bin/systemctl restart --no-block swarm-bao-operator-viewer-policy.service";
|
||||
ExecStartPost = "-${pkgs.systemd}/bin/systemctl restart --no-block swarm-bao-operator-viewer-policy.service swarm-bao-public-read-policy.service";
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
|
|
@ -3380,6 +3419,84 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
# `publicReadBlock` in bao's `default` policy. Read, the old block
|
||||
# dropped, the current one appended, and written back only when that
|
||||
# differs, so the store's own stanzas stay as the store wrote them.
|
||||
#
|
||||
# The granter may write `default` only once `swarm-bao-granter-role` has
|
||||
# run with a bootstrap token since that grant was added. Until then this
|
||||
# says so and exits 0, as the viewer unit does: principals granted a
|
||||
# `swarm/public/` path by name still read it, and a day of retries would
|
||||
# change nothing. `swarm-bao-granter-role` restarts this unit each time
|
||||
# it succeeds.
|
||||
systemd.services.swarm-bao-public-read-policy = lib.mkIf haveGranter {
|
||||
description = "let every bao token read swarm/public/ through the default policy";
|
||||
after = [
|
||||
"container@${cfg.machine}.service"
|
||||
"swarm-bao-controller-policy.service"
|
||||
]
|
||||
++ granterAfter;
|
||||
requires = [ "swarm-bao-pki.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = [
|
||||
baoCli
|
||||
pkgs.coreutils
|
||||
pkgs.gawk
|
||||
];
|
||||
environment = granterEnv;
|
||||
# Same unseal wait as its siblings above, for the reason stated there.
|
||||
startLimitBurst = 2880;
|
||||
startLimitIntervalSec = 90000;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
Restart = "on-failure";
|
||||
RestartSec = 30;
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
|
||||
${granterLogin}
|
||||
|
||||
caps="$(bao token capabilities sys/policies/acl/default)"
|
||||
case "$caps" in
|
||||
*update*) ;;
|
||||
*)
|
||||
echo "the granter may not write the store's default policy yet (capabilities: $caps), so swarm/public/ is readable only where a policy names it." >&2
|
||||
echo "one-time step, as root on this host (docs/getting-started/setup.md):" >&2
|
||||
${lib.concatMapStringsSep "\n" (l: "echo ${lib.escapeShellArg " ${l}"} >&2") granterSetupSteps}
|
||||
echo "swarm-bao-granter-role re-runs this unit once it succeeds." >&2
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! current="$(bao read -field=policy sys/policies/acl/default 2>"$err")"; then
|
||||
echo "could not read the store's default policy; leaving it as it is." >&2
|
||||
cat "$err" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Never write a default policy built from nothing: it would drop
|
||||
# every stanza the store's tokens rely on for their own lookups.
|
||||
if [ -z "$current" ]; then
|
||||
echo "the store returned an empty default policy; leaving it as it is." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
kept="$(printf '%s\n' "$current" |
|
||||
awk -v b=${lib.escapeShellArg publicReadBegin} -v e=${lib.escapeShellArg publicReadEnd} \
|
||||
'$0 == b { skip = 1 } !skip { print } $0 == e { skip = 0 }')"
|
||||
wanted="$kept
|
||||
${publicReadBlock}"
|
||||
if [ "$wanted" = "$current" ]; then
|
||||
echo "the default policy already lets every token read swarm/public/"
|
||||
exit 0
|
||||
fi
|
||||
printf '%s\n' "$wanted" |
|
||||
bao policy write default -
|
||||
echo "the default policy now lets every token read swarm/public/"
|
||||
'';
|
||||
};
|
||||
|
||||
# The CA bind source is written at runtime by a host unit, so the
|
||||
# container has to start after it — otherwise nspawn sets up a mount
|
||||
# over a file that does not exist yet.
|
||||
|
|
|
|||
Loading…
Reference in a new issue