hive-c0re: hand the daemon the store identity it cannot open itself
`credential.rs` calls `SecretStore::from_env`, and nothing set `BAO_*` for this unit — only `swarm-bao-matrix-token` had them. Fixing that alone would not have been enough: hive-c0re runs as hive-core, and glue-bao-tls mints the client key `0600` inside a `0700` directory, so the daemon cannot read the file even when it is named. Both halves go through LoadCredential, which this unit already uses for the swarm-status client secret: root reads the identity at unit start, hive-core sees it under `%d`, and no second on-disk copy exists. The gate is the identity, never `deploy.bao.enable` — a hive that reads a store on another machine holds a certificate and runs no store. Four module-eval cases: the co-located and off-host presence arms, the absence arm for a hive with no identity, and a presence/absence pair for the optional CA.
This commit is contained in:
parent
c6eedec3ed
commit
ad439843f1
3 changed files with 103 additions and 1 deletions
|
|
@ -13,6 +13,13 @@
|
|||
let
|
||||
cfg = config.services.hyperhive.c0re;
|
||||
|
||||
baoDeploy = config.services.hyperhive.deploy.bao;
|
||||
# Held in one place because the LoadCredential below and the `BAO_*`
|
||||
# environment in ./environment.nix have to agree on when they exist: a
|
||||
# credential with no reader is dead weight, and an environment naming a
|
||||
# credential nobody loaded is a daemon that fails at the TLS handshake.
|
||||
baoClientIdentity = baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null;
|
||||
|
||||
caTrust = import ../lib/hive-ca-trust.nix {
|
||||
inherit lib;
|
||||
tlsCfg = config.services.hyperhive.deploy.hive-controller.tls;
|
||||
|
|
@ -309,7 +316,23 @@ in
|
|||
# delivers into a container, across a filesystem boundary.)
|
||||
lib.optional (
|
||||
config.services.hyperhive.swarm.statusPublish.clientSecretFile != null
|
||||
) "swarm-status-client.secret:${config.services.hyperhive.swarm.statusPublish.clientSecretFile}";
|
||||
) "swarm-status-client.secret:${config.services.hyperhive.swarm.statusPublish.clientSecretFile}"
|
||||
# The secret store's client identity, on the same reasoning one
|
||||
# paragraph up — with a sharper edge: ./glue-bao-tls.nix mints the
|
||||
# key `0600` inside a `0700` directory, so hive-core cannot read it
|
||||
# at all. Loading it as a credential is what makes the store
|
||||
# reachable from an unprivileged daemon without widening either.
|
||||
# The gate is the identity, never `deploy.bao.enable`: a hive that
|
||||
# reads a store on another machine holds one of these and runs no
|
||||
# store. ./glue-matrix-bao-token.nix gates its own reader the same
|
||||
# way.
|
||||
++ lib.optionals baoClientIdentity [
|
||||
"bao-client.pem:${baoDeploy.clientCertFile}"
|
||||
"bao-client-key.pem:${baoDeploy.clientKeyFile}"
|
||||
]
|
||||
++ lib.optional (
|
||||
baoClientIdentity && baoDeploy.serverCaFile != null
|
||||
) "bao-ca.pem:${baoDeploy.serverCaFile}";
|
||||
# Sandboxing. hive-c0re is unprivileged (runs as hive-core, never
|
||||
# setuid), makes HTTP requests to forge/matrix/Anthropic (keeps INET),
|
||||
# and delegates all privileged ops to hive-priv via a Unix socket.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@
|
|||
}:
|
||||
let
|
||||
cfg = config.services.hyperhive.c0re;
|
||||
baoDeploy = config.services.hyperhive.deploy.bao;
|
||||
baoCfg = config.services.hyperhive.swarm.bao;
|
||||
# The same test ../glue-matrix-bao-token.nix applies, and for the same
|
||||
# reason: a reader is defined by holding a certificate the store accepts,
|
||||
# not by sharing a host with the store. Deriving this from
|
||||
# `deploy.bao.enable` would be the co-location assumption itself.
|
||||
haveBaoClientIdentity = baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null;
|
||||
in
|
||||
{
|
||||
# nix (the prebuild `nix build`, flake-check, and meta eval) writes
|
||||
|
|
@ -263,3 +270,23 @@ in
|
|||
# ./default.nix. The daemon reads a path, never a value.
|
||||
HIVE_C0RE_OIDC_CLIENT_SECRET_FILE = "%d/swarm-status-client.secret";
|
||||
}
|
||||
//
|
||||
# Where the swarm's secret store is, and the identity this hive presents to
|
||||
# it (hive-c0re::workers::credential). `swarm_secret_client` reads these
|
||||
# spellings explicitly rather than vaultrs's `VAULT_*` defaults — falling
|
||||
# through to those would build a client with no identity and fail at the TLS
|
||||
# handshake, naming neither.
|
||||
lib.optionalAttrs haveBaoClientIdentity {
|
||||
BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}";
|
||||
# `%d`, not the paths themselves: the key is `0600` root-owned and this
|
||||
# daemon runs as hive-core, so it never gets read access to the original.
|
||||
# See the LoadCredential in ./default.nix.
|
||||
BAO_CLIENT_CERT = "%d/bao-client.pem";
|
||||
BAO_CLIENT_KEY = "%d/bao-client-key.pem";
|
||||
}
|
||||
// lib.optionalAttrs (haveBaoClientIdentity && baoDeploy.serverCaFile != null) {
|
||||
# Absent means the system trust store — right for a deployment with a real
|
||||
# CA, wrong for the self-signed one ../glue-bao-tls.nix mints, which is why
|
||||
# that file names this path rather than leaving it to a default.
|
||||
BAO_CACERT = "%d/bao-ca.pem";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -483,6 +483,58 @@ let
|
|||
in
|
||||
s ? swarm-bao-matrix-token && s.swarm-bao-matrix-token.requires == [ "swarm-bao-pki.service" ];
|
||||
}
|
||||
{
|
||||
# hive-c0re runs as hive-core and the client key is `0600` root-owned
|
||||
# inside a `0700` directory, so the identity reaches the daemon as a
|
||||
# systemd credential and the environment names `%d` rather than the
|
||||
# file. Both halves are asserted together because either alone is a
|
||||
# daemon that fails at the TLS handshake, naming neither.
|
||||
name = "a reader hands hive-c0re a store identity the daemon cannot open itself";
|
||||
ok =
|
||||
let
|
||||
s = baoRemoteReader.systemd.services;
|
||||
in
|
||||
s ? hive-c0re
|
||||
&& (s.hive-c0re.environment.BAO_CLIENT_CERT or null) == "%d/bao-client.pem"
|
||||
&& (s.hive-c0re.environment.BAO_CLIENT_KEY or null) == "%d/bao-client-key.pem"
|
||||
&& builtins.elem "bao-client.pem:/etc/pki/bao-client.pem" s.hive-c0re.serviceConfig.LoadCredential
|
||||
&& builtins.elem "bao-client-key.pem:/etc/pki/bao-client-key.pem" s.hive-c0re.serviceConfig.LoadCredential;
|
||||
}
|
||||
{
|
||||
# Absence arm for the case above. A hive with no client identity gets no
|
||||
# store environment at all — the daemon reports a queue it cannot serve
|
||||
# rather than a handshake it cannot explain.
|
||||
name = "a hive with no client identity gives hive-c0re no store environment";
|
||||
ok =
|
||||
let
|
||||
s = matrixNoBaoIdentity.systemd.services;
|
||||
in
|
||||
s ? hive-c0re
|
||||
&& !(s.hive-c0re.environment ? BAO_ADDR)
|
||||
&& !(lib.any (c: lib.hasPrefix "bao-" c) s.hive-c0re.serviceConfig.LoadCredential);
|
||||
}
|
||||
{
|
||||
# The CA is its own arm: absent means the system trust store, which is
|
||||
# right for a deployment with a real CA and wrong for a self-signed one.
|
||||
name = "a reader that names no store CA falls through to the system trust store";
|
||||
ok =
|
||||
let
|
||||
s = baoRemoteReader.systemd.services;
|
||||
in
|
||||
s ? hive-c0re && !(s.hive-c0re.environment ? BAO_CACERT);
|
||||
}
|
||||
{
|
||||
# Presence control for the arm above: the CA is conditional, not gone.
|
||||
# Co-located, ./host-modules/glue-bao-tls.nix mints one and names it.
|
||||
name = "a reader beside a self-signed store is given that store's CA";
|
||||
ok =
|
||||
let
|
||||
s = baoWithMatrix.systemd.services;
|
||||
in
|
||||
s ? hive-c0re
|
||||
&& (s.hive-c0re.environment.BAO_CACERT or null) == "%d/bao-ca.pem"
|
||||
&& lib.any (c: lib.hasPrefix "bao-ca.pem:" c) s.hive-c0re.serviceConfig.LoadCredential;
|
||||
}
|
||||
{
|
||||
# The name a reader dials has to resolve where the store runs; a
|
||||
# multi-host swarm resolves it upstream instead.
|
||||
|
|
|
|||
Loading…
Reference in a new issue