Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
3 changed files with 9 additions and 111 deletions

View file

@ -62,27 +62,11 @@ authenticate until some role exists — this token is what breaks that cycle,
and it's the only step that needs the root token.
```bash
# Exactly the four grants the bootstrap unit needs, and nothing else. Each was
# derived with `bao <cmd> -output-policy`, which prints what a command requires
# without running it.
# A policy that can write exactly one policy, and nothing else.
bao policy write swarm-bootstrap - <<'EOF'
path "sys/policies/acl/swarm-controller" {
capabilities = ["create", "update"]
}
# Cert auth is a mount, and nothing has created it yet: reading `sys/auth` is
# how the unit checks, and `sudo` is what enabling one costs.
path "sys/auth" {
capabilities = ["read"]
}
path "sys/auth/cert" {
capabilities = ["create", "update", "sudo"]
}
path "auth/cert/certs/swarm-controller" {
capabilities = ["create", "update"]
}
EOF
# A token holding it. `-orphan` so it outlives the session that made it.
@ -91,17 +75,7 @@ bao token create -policy=swarm-bootstrap -ttl=24h -orphan -display-name=swarm-bo
Put the token's value at `services.hyperhive.deploy.bao.bootstrapTokenFile`
(all-local names that path for you), then rebuild. A one-shot unit inside the
store's container reads it, writes the `swarm-controller` policy, enables the
cert auth method, and creates the `swarm-controller` role that attaches the two.
⚠️ **None of this has been run against a live store.** Nothing in the tree has
ever authenticated to OpenBao, so treat the block above as derived rather than
exercised — the grants come from `-output-policy`, not from a swarm that came
up on them.
⚠️ **The role it creates has nothing to present a certificate for yet.** No
deployment shape mints a leaf whose CN reads `swarm-controller`, so until one
does, the role stays provisioning that waits for a consumer.
store's container reads it and writes the `swarm-controller` policy.
**Delete the file once that has run.** The unit skips when it's absent, so a
host that has finished bootstrapping stops carrying the credential — and the

View file

@ -150,15 +150,10 @@ let
bootstrapTokenDir =
if haveBootstrapToken then builtins.dirOf baoDeploy.bootstrapTokenFile else null;
# The name both ends must agree on: the cert-auth role below attaches this
# policy by spelling it the same way, and is itself named after it.
# The name both ends must agree on: whoever later gives the controller a
# cert-auth role attaches this policy by spelling it the same way.
controllerPolicyName = "swarm-controller";
# The subject the controller's certificate must carry. Cert auth matches on
# the CN, so this is an interface rather than a label: a leaf signed by the
# right CA but minted with any other subject cannot authenticate.
controllerCn = "swarm-controller";
# Derived with `bao write -output-policy`, which short-circuits the request
# and needs no server, rather than written from memory.
#
@ -1004,7 +999,7 @@ in
# where the store is reachable without a client certificate — which
# is the point, since no role exists yet to issue one against.
systemd.services.swarm-bao-controller-policy = lib.mkIf haveBootstrapToken {
description = "write the swarm controller's bao policy and cert-auth role";
description = "write the swarm controller's bao policy";
after = [ "openbao.service" ];
wantedBy = [ "multi-user.target" ];
path = [
@ -1039,37 +1034,6 @@ in
# than failing on one that already exists.
printf '%s' ${lib.escapeShellArg controllerPolicyText} |
bao policy write ${lib.escapeShellArg controllerPolicyName} -
''
+ lib.optionalString (baoDeploy.clientCaFile != null) ''
# The policy above grants paths under `auth/cert/`, and nothing
# in this tree creates that mount. Without this, the grant names
# a location that does not exist and every certificate login
# fails — the controller's own, and the per-hive ones it later
# issues against the same mount.
#
# Asked rather than attempted: `auth enable` errors on a mount
# that already exists, and recognising that would tie a rebuild
# to an error string we have never seen this store emit.
mounted="$(bao auth list -format=json)"
case "$mounted" in
*'"cert/"'*) ;;
*) bao auth enable cert ;;
esac
# `certificate=` is the CA, so this role trusts every leaf that
# CA signed and `allowed_common_names` is the whole of what
# narrows it to one identity. ⚠️ The same CA signs each hive's
# reader leaf with CN = the hive's name, so a hive named
# `${controllerCn}` would satisfy this role.
#
# Named outside the `hive-*` namespace the policy grants, so the
# controller cannot rewrite the role that constrains it.
bao write auth/cert/certs/${lib.escapeShellArg controllerPolicyName} \
certificate=@${tlsDir}/client-ca.pem \
allowed_common_names=${lib.escapeShellArg controllerCn} \
token_policies=${lib.escapeShellArg controllerPolicyName} \
display_name=${lib.escapeShellArg controllerCn}
'';
};
};

View file

@ -254,15 +254,6 @@ let
baoGrantNoStore = hive {
deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token";
};
# The store and the token, with no CA to trust. `mkForce` because the PKI
# glue supplies one by default here — this is the deployment that brings its
# own certificates and has not named the authority yet, and it separates
# "the grant unit runs" from "cert auth can be set up".
baoGrantNoClientCa = hive {
deploy.bao.enable = true;
deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token";
deploy.bao.clientCaFile = lib.mkForce null;
};
baoNames = machine: machine.services.hyperhive.gateway.localNames;
@ -515,37 +506,10 @@ let
lib.hasInfix "sys/policies/acl/hive-*" s && !(lib.hasInfix "sys/policies/acl/*" s);
}
{
# The policy above grants paths under a mount nothing else creates, so
# the unit that writes the policy has to create it too — otherwise every
# certificate login fails against a path that is not there.
name = "the granting unit creates the cert auth mount and the controller's role";
ok =
let
s = baoGrantHere.containers.swarm-bao.config.systemd.services.swarm-bao-controller-policy.script;
in
lib.hasInfix "bao auth enable cert" s
&& lib.hasInfix "auth/cert/certs/swarm-controller" s
&& lib.hasInfix "/var/lib/swarm-bao-tls/client-ca.pem" s;
}
{
# The arm that makes the one above mean something. A role's trust anchor
# is the CA, so with none named there is nothing to write — and the
# policy write, which needs no CA, must survive that.
#
# ⚠️ Matched on the COMMANDS, not on `auth/cert/certs`: the policy text is
# embedded in this same script and grants that very path, so the shorter
# infix is present either way and the arm could never fail.
name = "with no client CA the unit still writes the policy and skips the role";
ok =
let
s =
baoGrantNoClientCa.containers.swarm-bao.config.systemd.services.swarm-bao-controller-policy.script;
in
lib.hasInfix "bao policy write" s
&& !(lib.hasInfix "bao auth enable cert" s)
&& !(lib.hasInfix "client-ca.pem" s);
}
{
# The arm that makes the one above mean something, and the property the
# host-side half depends on: no store here, so no bind mount and no unit.
# Without it a hive that merely names a token would drag the store's
# container config into its evaluation.
# Reads the vhost's rendered `root`, not the option: the UI is served
# straight out of a store path, so a shim that resolves but stops
# reaching the module would leave nginx pointing at the default build.
@ -583,10 +547,6 @@ let
== pkgs.emptyDirectory;
}
{
# What makes the granting-unit cases mean something, and the property
# the host-side half depends on: no store here, so no bind mount and no
# unit. Without it a hive that merely names a token would drag the
# store's container config into its evaluation.
name = "a bootstrap token on a host that runs no store grants nothing";
ok = !(baoGrantNoStore.systemd.services ? swarm-bao-bootstrap-dir);
}