fix(#3860): provision the bao pkcs11 token inside its container
openbao runs as a DynamicUser, so the uid that has to open the token
store is allocated by the container's PID 1 and cannot be named by a
host unit — the store was created root-owned 0700 and the seal could
never read it. The provisioning unit moves inside the container and
hands the sqlite store over by group; the host keeps only the mkdir the
bind mount needs, create-only so a reboot does not re-impose 0700.
Two further layers blocked the same start, both measured while fixing
this one:
- DynamicUser implies ProtectSystem=strict (systemd.exec(5)), so the
bind mount was read-only to openbao however it was owned, and the
pkcs11 library opens its store read-write. ReadWritePaths= is
required and was absent.
- allowedDevices renders DeviceAllow= and nothing else, and nspawn
builds its own /dev as a fixed tmpfs and cannot create device nodes
— verified against a live container, whose /dev holds no host
devices at all. /dev/tpmrm0 was therefore absent inside swarm-bao,
not merely unpermitted. It is now bound in.
Whether openbao's dynamic uid may *open* that node is a third question:
the tss gid is dynamically allocated, so no name or number means the
same thing on both sides of the boundary. Filed separately rather than
guessed at here.
The two module-eval cases that asserted the unit on the host now assert
it in the container and absent from the host, and two new cases pin the
device bind and the write access — each was individually valid and
collectively required, which is the state no assertion catches. Both
new cases select with `or [ ]`: mutation-testing them showed that a bare
select aborts the run with a nix trace instead of failing the case by
name, which also hid the second failure behind the first.
This commit is contained in:
parent
8a3766c60b
commit
a58c7af3bd
2 changed files with 145 additions and 52 deletions
|
|
@ -129,6 +129,10 @@ let
|
|||
# the daemon's own startup is otherwise the first reader.
|
||||
baoSettings = machine: machine.containers.swarm-bao.config.services.openbao.settings;
|
||||
|
||||
# The store's units live inside its container, so the gates below have to
|
||||
# look there rather than at the host's service set.
|
||||
baoUnits = machine: machine.containers.swarm-bao.config.systemd.services;
|
||||
|
||||
# A priority collision is a property of the *option*, not
|
||||
# of the merged value's interior — nix throws the moment the value is
|
||||
# demanded at all, so `seq`-ing each `serviceConfig` value to WHNF is
|
||||
|
|
@ -193,19 +197,50 @@ let
|
|||
ok = forceCiServiceConfigs;
|
||||
}
|
||||
{
|
||||
# The store's seal is spread over five gates — the stanza, the
|
||||
# provisioning unit, a bind mount, a device and an EnvironmentFile.
|
||||
# The store's seal is spread over six gates — the stanza, the
|
||||
# provisioning unit, two bind mounts, a device and an EnvironmentFile.
|
||||
# Rendering only some of them is the dangerous state: a store that
|
||||
# says hardware-backed and seals with a software key, which no
|
||||
# assertion can catch because every value is individually valid.
|
||||
name = "a shamir store renders no TPM provisioning unit";
|
||||
ok = !(baoShamir.systemd.services ? swarm-bao-token);
|
||||
ok = !(baoUnits baoShamir ? swarm-bao-token);
|
||||
}
|
||||
{
|
||||
# Presence control for the case above. Without it, a typo in the
|
||||
# option name would satisfy the absence arm forever.
|
||||
name = "a pkcs11 store renders the TPM provisioning unit";
|
||||
ok = baoPkcs11.systemd.services ? swarm-bao-token;
|
||||
# option name would satisfy the absence arm forever. The second half is
|
||||
# the fix itself: the unit has to run where openbao's `DynamicUser` is
|
||||
# allocated, and a host unit writing the same bytes has no name to hand
|
||||
# them to.
|
||||
name = "a pkcs11 store provisions the token in the container, not on the host";
|
||||
ok = (baoUnits baoPkcs11 ? swarm-bao-token) && !(baoPkcs11.systemd.services ? swarm-bao-token);
|
||||
}
|
||||
{
|
||||
# `allowedDevices` renders `DeviceAllow=` and nothing else — nspawn
|
||||
# mounts its own /dev and cannot create device nodes, so permission to
|
||||
# use a device that was never bound in opens nothing. Neither half
|
||||
# fails on its own, which is why they are asserted as a pair.
|
||||
name = "a pkcs11 store gets the TPM device bound in, not merely allowed";
|
||||
ok =
|
||||
let
|
||||
c = baoPkcs11.containers.swarm-bao;
|
||||
in
|
||||
(c.bindMounts ? "/dev/tpmrm0") && builtins.any (d: d.node == "/dev/tpmrm0") c.allowedDevices;
|
||||
}
|
||||
{
|
||||
# `DynamicUser` implies `ProtectSystem=strict`, so the token directory is
|
||||
# read-only to the seal however it is owned, and the group is the only
|
||||
# handle on a uid allocated at start. Dropping either surfaces as a
|
||||
# pkcs11 error deep in a library, naming neither the mount nor the user.
|
||||
name = "the store's seal may write the token directory, and is in its group";
|
||||
ok =
|
||||
let
|
||||
sc = (baoUnits baoPkcs11).openbao.serviceConfig;
|
||||
in
|
||||
# `or [ ]` rather than a bare select: the interesting mutation is the
|
||||
# key being gone, and a select would abort the whole run with a nix
|
||||
# trace instead of failing this case by name.
|
||||
builtins.elem "/var/lib/swarm-bao-token" (sc.ReadWritePaths or [ ])
|
||||
&& builtins.elem "swarm-bao-token" (sc.SupplementaryGroups or [ ]);
|
||||
}
|
||||
{
|
||||
# The store's mTLS identity is a separate trust domain from both CAs in
|
||||
|
|
|
|||
Loading…
Reference in a new issue