Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
6 changed files with 20 additions and 142 deletions

View file

@ -67,14 +67,11 @@ Whether anything more is needed depends on
The store serves TLS, and on a hive that deploys it you need do nothing: a The store serves TLS, and on a hive that deploys it you need do nothing: a
first-boot unit mints a CA of the store's own plus the two leaves it signs — first-boot unit mints a CA of the store's own plus the two leaves it signs —
the store's server certificate and this host's client certificate — and points the store's server certificate and this host's client certificate — and points
`deploy.bao.serverCertFile`, `.serverKeyFile` and `.clientCaFile` at the store's `deploy.bao.serverCertFile`, `.serverKeyFile` and `.clientCaFile` at them.
half, `.clientCertFile`, `.clientKeyFile` and `.serverCaFile` at the reader's.
Those are `mkDefault`s, so naming your own paths wins. Do that when your Those are `mkDefault`s, so naming your own paths wins. Do that when your
certificates come from a real internal CA; the store has no opinion about certificates come from a real internal CA; the store has no opinion about
which. A hive that does **not** deploy the store names the reader's three which. ⚠️ Not the gateway's HTTPS certificates and not the hive CA — this is
itself: that leaf is issued out of band, and it is the one credential the store
cannot hand you, being what opens it. ⚠️ Not the gateway's HTTPS certificates and not the hive CA — this is
**mTLS between services and the store**, a separate trust domain, because a **mTLS between services and the store**, a separate trust domain, because a
store that took its identity from an authority it will itself distribute could store that took its identity from an authority it will itself distribute could
never come up before that authority. never come up before that authority.

View file

@ -156,10 +156,6 @@ mints a CA that signs exactly two things, the store's server certificate and a
reader's client certificate, and distributes nothing. A deployment with a real reader's client certificate, and distributes nothing. A deployment with a real
internal CA deletes that file and names its own paths in internal CA deletes that file and names its own paths in
`deploy.bao.serverCertFile` / `clientCaFile`; the store itself has no opinion. `deploy.bao.serverCertFile` / `clientCaFile`; the store itself has no opinion.
A hive that reads from a store on **another** machine names the reader's half —
`clientCertFile`, `clientKeyFile`, `serverCaFile` — and places that leaf by hand.
It is the one credential that cannot come out of the store, being what opens it;
everything else a hive needs does.
## The constraint that decides where the root lives ## The constraint that decides where the root lives

View file

@ -73,14 +73,6 @@ in
serverCertFile = lib.mkDefault "${pkiDir}/server.pem"; serverCertFile = lib.mkDefault "${pkiDir}/server.pem";
serverKeyFile = lib.mkDefault "${pkiDir}/server-key.pem"; serverKeyFile = lib.mkDefault "${pkiDir}/server-key.pem";
clientCaFile = lib.mkDefault "${pkiDir}/ca.pem"; clientCaFile = lib.mkDefault "${pkiDir}/ca.pem";
# A reader on this host, which happens to be the host that mints. Only
# these three are what a reader elsewhere needs placed by hand; that they
# collapse to the same CA file here is a property of self-signing, not of
# the pairing.
clientCertFile = lib.mkDefault "${pkiDir}/client.pem";
clientKeyFile = lib.mkDefault "${pkiDir}/client-key.pem";
serverCaFile = lib.mkDefault "${pkiDir}/ca.pem";
}; };
# Idempotent on ABSENCE, never on content. Re-issuing the CA invalidates # Idempotent on ABSENCE, never on content. Re-issuing the CA invalidates

View file

@ -16,11 +16,11 @@
# overwrites it with the swarm's copy when the store has one. A store that is # overwrites it with the swarm's copy when the store has one. A store that is
# empty or unreachable leaves a working hive with a local token. # empty or unreachable leaves a working hive with a local token.
# #
# 📌 This runs wherever a client identity is configured, NOT only where the # 📌 LIMIT, stated rather than hidden: this gates on the store running HERE.
# store is. `deploy.bao.enable` would have been the co-location assumption # A hive reading from a store on another machine needs the same unit with that
# itself; the reader needs a certificate, not a neighbour. On the store's own # machine's address and a client leaf issued out of band — the mechanism is
# host ./glue-bao-tls.nix supplies one as a `mkDefault` and nothing changes; # identical, only `-address` and the cert's provenance differ. Deferred until
# elsewhere an operator places the leaf and names it, and the same unit works. # there is a second hive to test it against, rather than shipped untested.
{ {
pkgs, pkgs,
lib, lib,
@ -33,12 +33,11 @@ let
baoCfg = hyperhiveCfg.swarm.bao; baoCfg = hyperhiveCfg.swarm.bao;
matrixCfg = hyperhiveCfg.swarm.matrix; matrixCfg = hyperhiveCfg.swarm.matrix;
baoDeploy = deployCfg.bao; # Owned by ./glue-bao-tls.nix, which mints them. Named here rather than
# shared through a `let`: a cross-module binding would make these two files
# What decides whether this unit exists at all. A reader is defined by holding # one file with a gap in the middle, and the whole point of a glue module is
# a certificate the store accepts, and that is true on the store's own host # that it can be deleted on its own.
# and on a hive three networks away for exactly the same reason. pkiDir = "/var/lib/swarm-bao-pki";
haveClientIdentity = baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null;
# Where the token lives in the store. A path, not a convention to guess at: # Where the token lives in the store. A path, not a convention to guess at:
# whoever writes it and whoever reads it must agree, and the agreement # whoever writes it and whoever reads it must agree, and the agreement
@ -53,19 +52,15 @@ let
matrixMachine = "hive-matrix"; matrixMachine = "hive-matrix";
in in
{ {
config = lib.mkIf (hyperhiveCfg.enable && haveClientIdentity && deployCfg.matrix.enable) { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.bao.enable && deployCfg.matrix.enable) {
systemd.services.swarm-bao-matrix-token = { systemd.services.swarm-bao-matrix-token = {
description = "fetch the matrix registration token from the swarm secret store"; description = "fetch the matrix registration token from the swarm secret store";
# Every one of these names a unit that exists only where the store runs. after = [
# `Requires=` on an absent unit fails the job outright, so the ordering is
# conditional even though the read is not: off-host there is nothing local
# to wait for, and the timeout below is what bounds the attempt instead.
after = lib.optionals baoDeploy.enable [
"swarm-bao-pki.service" "swarm-bao-pki.service"
"container@${baoCfg.machine}.service" "container@${baoCfg.machine}.service"
]; ];
wants = lib.optionals baoDeploy.enable [ "container@${baoCfg.machine}.service" ]; wants = [ "container@${baoCfg.machine}.service" ];
requires = lib.optionals baoDeploy.enable [ "swarm-bao-pki.service" ]; requires = [ "swarm-bao-pki.service" ];
before = [ "container@${matrixMachine}.service" ]; before = [ "container@${matrixMachine}.service" ];
wantedBy = [ "container@${matrixMachine}.service" ]; wantedBy = [ "container@${matrixMachine}.service" ];
path = [ path = [
@ -82,13 +77,9 @@ in
}; };
environment = { environment = {
BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}"; BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}";
BAO_CLIENT_CERT = baoDeploy.clientCertFile; BAO_CACERT = "${pkiDir}/ca.pem";
BAO_CLIENT_KEY = baoDeploy.clientKeyFile; BAO_CLIENT_CERT = "${pkiDir}/client.pem";
} BAO_CLIENT_KEY = "${pkiDir}/client-key.pem";
# Absent means the system trust store, which is what a deployment with a
# real CA wants and what a self-signed one must not be left with.
// lib.optionalAttrs (baoDeploy.serverCaFile != null) {
BAO_CACERT = baoDeploy.serverCaFile;
}; };
script = '' script = ''
set -euo pipefail set -euo pipefail

View file

@ -268,54 +268,6 @@ in
''; '';
}; };
clientCertFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/var/lib/swarm-bao/client.pem";
description = ''
Certificate a **reader on this machine** presents to the store.
The counterpart to {option}`services.hyperhive.deploy.bao.clientCaFile`,
which is the store's side of the same handshake. Only the store's side
was declarable, so a hive that did not run the store had no way to be
pointed at a certificate even when one was placed for it.
On a hive that runs the store, a glue module supplies the leaf it minted,
as a `mkDefault`. Everywhere else this is the credential an operator
places by hand — the one secret that cannot come out of the store,
because it is what opens it.
A path, never a value.
'';
};
clientKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/var/lib/swarm-bao/client-key.pem";
description = ''
Private key for {option}`services.hyperhive.deploy.bao.clientCertFile`.
Both or neither — a certificate with no key authenticates nothing.
'';
};
serverCaFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/var/lib/swarm-bao/ca.pem";
description = ''
Authority a **reader on this machine** validates the store's certificate
against.
Not {option}`services.hyperhive.deploy.bao.clientCaFile` with the words
rearranged: that one is the store choosing which readers to trust, this
one is a reader choosing which store to trust. A deployment that
self-signs both ends points them at the same file and reads that as
confirmation they are interchangeable — they are not, and they diverge
the moment either end gets a real CA.
'';
};
extraListenAddresses = lib.mkOption { extraListenAddresses = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
default = [ ]; default = [ ];

View file

@ -90,18 +90,6 @@ let
deploy.bao.enable = true; deploy.bao.enable = true;
deploy.matrix.enable = true; deploy.matrix.enable = true;
}; };
# A hive that reads from a store it does not run: no `deploy.bao.enable`, so
# nothing here mints a leaf and the operator names one placed by hand. The
# deployment this pairing exists to serve, and the one that was previously
# inexpressible — the gate asked whether the store was a neighbour.
baoRemoteReader = hive {
deploy.matrix.enable = true;
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
};
# The same hive with the identity taken away, which separates "a homeserver
# is deployed" from "this host can authenticate to the store".
matrixNoBaoIdentity = hive { deploy.matrix.enable = true; };
# A priority collision is a property of the *option*, not # A priority collision is a property of the *option*, not
# of the merged value's interior — nix throws the moment the value is # of the merged value's interior — nix throws the moment the value is
@ -217,48 +205,10 @@ let
name = "a store with no homeserver beside it renders no token reader"; name = "a store with no homeserver beside it renders no token reader";
ok = !(baoPkcs11.systemd.services ? swarm-bao-matrix-token); ok = !(baoPkcs11.systemd.services ? swarm-bao-matrix-token);
} }
{
name = "a hive that names a client identity reads from a store it does not run";
ok = baoRemoteReader.systemd.services ? swarm-bao-matrix-token;
}
{
# Absence arm for the one above, and the reason the gate is the identity
# rather than the homeserver: without it, deploying matrix anywhere would
# render a reader that cannot authenticate.
name = "a homeserver with no way to authenticate to the store renders no token reader";
ok = !(matrixNoBaoIdentity.systemd.services ? swarm-bao-matrix-token);
}
{
# `Requires=` on a unit that does not exist fails the job, and nothing
# local mints certificates off-host — so this orders against nothing.
# Eval cannot see that failure; only the empty list here stands in for it.
name = "an off-host reader requires no unit the store's host would have provided";
# Membership first, then the value: indexing a missing unit throws, and a
# table that reports which property broke must not be the thing that dies.
ok =
let
s = baoRemoteReader.systemd.services;
in
s ? swarm-bao-matrix-token && s.swarm-bao-matrix-token.requires == [ ];
}
{
# Presence control for the case above: the list is conditional, not gone.
name = "a co-located reader still orders after the local pki unit";
ok =
let
s = baoWithMatrix.systemd.services;
in
s ? swarm-bao-matrix-token && s.swarm-bao-matrix-token.requires == [ "swarm-bao-pki.service" ];
}
]; ];
bad = builtins.filter (c: !c.ok) cases; bad = builtins.filter (c: !c.ok) cases;
# Escaped, because a name is prose and prose contains apostrophes. Hand-quoting report = lib.concatMapStringsSep "\n" (c: " echo 'FAILED: ${c.name}' >&2") bad;
# broke the builder mid-report on the first such name that failed — and only
# ever on failure, so every green run agreed the reporter was fine.
report = lib.concatMapStringsSep "\n" (
c: " echo ${lib.escapeShellArg "FAILED: ${c.name}"} >&2"
) bad;
in in
# The results are embedded in the builder text on purpose: that is what # The results are embedded in the builder text on purpose: that is what
# makes this derivation's hash depend on them, so a nix-only change that # makes this derivation's hash depend on them, so a nix-only change that