Rewrite the remaining narration in files the previous commit touched: "moved"/"sat"/"was REMOVED"/"old default" comments in swarm-bao, swarm-otel, glue-bao-tls, nats-authelia, swarm-controller, swarm-ui, swarm-otel-service, swarm-authelia-service, the deploy.nix rename table, hive-ci, hive-forge/service, hive-tls, hyperhive, swarm-nats, swarm.nix and a few module-eval fixtures, so each says where a thing is or why it is so now. Revert the comment edits inside shell script strings in hive-tls, swarm-nats and the forge token mint to their previous text, so those generated units are unchanged. The only unit still changing is swarm-bao-pki (two comment lines in its script). Refs #3902
350 lines
16 KiB
Nix
350 lines
16 KiB
Nix
# `checks.module-eval-nats-authelia` — see ./lib.nix for the shared
|
|
# rationale (why this suite exists, naming convention, "evaluates
|
|
# not executes").
|
|
{
|
|
pkgs,
|
|
lib,
|
|
self,
|
|
nixosSystem,
|
|
}:
|
|
let
|
|
inherit
|
|
(import ./lib.nix {
|
|
inherit
|
|
pkgs
|
|
lib
|
|
self
|
|
nixosSystem
|
|
;
|
|
})
|
|
hive
|
|
runGroup
|
|
;
|
|
|
|
# The queue's callout identity, fourth split slice. `autoGenerateCallout` is
|
|
# left FALSE on purpose: that is what makes the seed paths the thing deciding
|
|
# `responderConfigured`, so the assertion below is about the seeds rather
|
|
# than about the auto-mint branch. Every one of the seven old paths is
|
|
# defined — `enable` included, which is why it is spelled the old way here
|
|
# while the fixture below uses the new one — so dropping any single nats
|
|
# shim fails the eval, not just the arms read.
|
|
natsOldPath = hive {
|
|
swarm.nats.enable = true;
|
|
swarm.nats.autoGenerateCallout = false;
|
|
swarm.nats.calloutUserPublicKey = "UTESTUSERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
|
swarm.nats.calloutIssuerPublicKey = "ATESTISSUERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
|
swarm.nats.calloutUserSeedFile = "/run/secrets/nats-user.seed";
|
|
swarm.nats.calloutIssuerSeedFile = "/run/secrets/nats-issuer.seed";
|
|
swarm.nats.authPackage = pkgs.emptyDirectory;
|
|
};
|
|
|
|
# Seventh split slice, plus slice 10's two authelia packages. Of slice 7's
|
|
# movers only `usersFile` has a rename entry — the other two are `readOnly`,
|
|
# and a rename module contributes a definition, which a read-only option
|
|
# refuses; see ./host-modules/deploy.nix. `package` and `bridgePackage` are
|
|
# ordinary options, so they do carry one. The two arms
|
|
# below have different jobs. `usersFile` tests the rename; the nats one tests
|
|
# that a reader pointed at the `deploy` namespace still renders the derived
|
|
# path — seven of those reads go through an alias a path-shaped grep
|
|
# cannot see.
|
|
autheliaOldPath = hive {
|
|
deploy.authelia.enable = true;
|
|
deploy.nats.enable = true;
|
|
swarm.authelia.usersFile = "/var/lib/test-authelia/users.yml";
|
|
swarm.authelia.package = pkgs.emptyDirectory;
|
|
swarm.authelia.bridgePackage = pkgs.emptyDirectory;
|
|
swarm.nats.autoGenerateCallout = false;
|
|
swarm.nats.calloutUserPublicKey = "UTESTUSERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
|
swarm.nats.calloutIssuerPublicKey = "ATESTISSUERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
|
swarm.nats.calloutUserSeedFile = "/run/secrets/nats-user.seed";
|
|
swarm.nats.calloutIssuerSeedFile = "/run/secrets/nats-issuer.seed";
|
|
};
|
|
|
|
# The queue with a store identity for its responder, placed by hand: the
|
|
# queue host that is not the store's.
|
|
natsWithStoreIdentity = hive {
|
|
deploy.nats.enable = true;
|
|
deploy.nats.autoGenerateCallout = false;
|
|
deploy.nats.calloutUserPublicKey = "UTESTUSERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
|
deploy.nats.calloutIssuerPublicKey = "ATESTISSUERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
|
deploy.nats.calloutUserSeedFile = "/run/secrets/nats-user.seed";
|
|
deploy.nats.calloutIssuerSeedFile = "/run/secrets/nats-issuer.seed";
|
|
deploy.nats.authPackage = pkgs.emptyDirectory;
|
|
deploy.nats.authBaoClientCertFile = "/etc/pki/nats-auth.pem";
|
|
deploy.nats.authBaoClientKeyFile = "/etc/pki/nats-auth-key.pem";
|
|
};
|
|
|
|
# The queue on the store's own host, where the PKI glue mints every leaf.
|
|
natsOnStoreHost = hive {
|
|
deploy.bao.enable = true;
|
|
deploy.nats.enable = true;
|
|
};
|
|
|
|
responderOf = h: h.containers.swarm-nats.config.systemd.services.swarm-nats-auth;
|
|
|
|
# A hive running NOTHING of the swarm's own services — no IdP here, no
|
|
# `swarm.authelia.url` set by hand. The whole point of the fixture is what it
|
|
# does *not* say: no IdP address is given and none is derived from
|
|
# co-location.
|
|
autheliaNotColocated = hive { };
|
|
# The same swarm's IdP host, for the arm that the two agree.
|
|
autheliaColocated = hive { deploy.authelia.enable = true; };
|
|
|
|
cases = [
|
|
{
|
|
# Reads the RENDERED settings, not the option: `calloutBlocks {…} // {
|
|
# … }` is a shallow merge, and a future edit that dropped or shadowed
|
|
# this key would still evaluate cleanly — the only reader that would
|
|
# notice is a publisher whose row exceeds upstream's much smaller
|
|
# default, and by then it is a dropped row, not an eval failure.
|
|
# Piggybacks on the pre-rename nats fixture above, which already
|
|
# renders this container's full config.
|
|
name = "the queue's payload ceiling is set, not inherited from the server's default";
|
|
ok = natsOldPath.containers.swarm-nats.config.services.nats.settings.max_payload == 8388608;
|
|
}
|
|
{
|
|
# The responder refuses the hive's shared agent client and takes no flag
|
|
# granting it subjects: an `--agent-publish-subject` fails the unit at
|
|
# start.
|
|
name = "the responder grants the hive's shared agent client nothing";
|
|
ok =
|
|
let
|
|
exec =
|
|
natsOldPath.containers.swarm-nats.config.systemd.services.swarm-nats-auth.serviceConfig.ExecStart;
|
|
in
|
|
!(lib.hasInfix "--agent-publish-subject " exec);
|
|
}
|
|
{
|
|
# The per-agent grant: the terminal and agent-state streams keyed on the
|
|
# agent alone, with the doubled dollar, and the role the store writes for
|
|
# it.
|
|
name = "the responder grants a verified agent its own hive-free subjects";
|
|
ok =
|
|
let
|
|
exec = (responderOf natsOldPath).serviceConfig.ExecStart;
|
|
in
|
|
lib.hasInfix "--agent-token-publish-subject '$$SWARM.term.{agent}'" exec
|
|
&& lib.hasInfix "--agent-token-publish-subject '$$SWARM.agent-state.{agent}'" exec
|
|
&& lib.hasInfix "--store-cert-role swarm-nats-auth" exec;
|
|
}
|
|
{
|
|
# Its own arm, like the two above: the icon write is a separate feature
|
|
# and dropping it must not hide behind the streams still being granted.
|
|
name = "the responder grants a verified agent its own icon key";
|
|
ok =
|
|
let
|
|
exec = (responderOf natsOldPath).serviceConfig.ExecStart;
|
|
in
|
|
lib.hasInfix "--agent-token-publish-subject '$$KV.agent-icons.{agent}'" exec;
|
|
}
|
|
{
|
|
# The whole per-agent grant, compared as a list rather than by infix: a
|
|
# wider subject added beside these (`$$JS.API.>`, another stream's name)
|
|
# would pass every presence check above.
|
|
name = "a verified agent's grant is exactly its own subjects, and its one JetStream API subject reads its own schedule key";
|
|
ok =
|
|
let
|
|
args = lib.splitString " " (responderOf natsOldPath).serviceConfig.ExecStart;
|
|
granted = lib.concatLists (
|
|
lib.imap0 (
|
|
i: a:
|
|
lib.optional (a == "--agent-token-publish-subject" && i + 1 < lib.length args) (
|
|
lib.elemAt args (i + 1)
|
|
)
|
|
) args
|
|
);
|
|
in
|
|
granted == [
|
|
"'$$SWARM.term.{agent}'"
|
|
"'$$SWARM.agent-state.{agent}'"
|
|
"'$$KV.agent-icons.{agent}'"
|
|
"'$$SWARM.term.{agent}.sub.>'"
|
|
"'$$JS.API.DIRECT.GET.KV_agent-schedules.$$KV.agent-schedules.{agent}'"
|
|
];
|
|
}
|
|
{
|
|
# Each credential by `LoadCredential`, and the environment naming where
|
|
# the unit sees it: a DynamicUser cannot read the copies directly.
|
|
name = "a responder with a store identity loads it and is pointed at the store";
|
|
ok =
|
|
let
|
|
u = responderOf natsWithStoreIdentity;
|
|
creds = u.serviceConfig.LoadCredential;
|
|
in
|
|
lib.elem "bao-client.pem:/var/lib/swarm-nats-auth/bao-client.pem" creds
|
|
&& lib.elem "bao-client-key.pem:/var/lib/swarm-nats-auth/bao-client-key.pem" creds
|
|
&& u.environment.BAO_CLIENT_CERT == "/run/credentials/swarm-nats-auth.service/bao-client.pem"
|
|
&& u.environment.BAO_CLIENT_KEY == "/run/credentials/swarm-nats-auth.service/bao-client-key.pem"
|
|
&& lib.hasPrefix "https://" u.environment.BAO_ADDR;
|
|
}
|
|
{
|
|
# An absent leaf must not stop the responder's agent-token path: it is
|
|
# delivered empty, beside the two seeds.
|
|
name = "the copy unit delivers the store identity, and an absent one as an empty file";
|
|
ok =
|
|
let
|
|
s = natsWithStoreIdentity.systemd.services.swarm-nats-auth-secrets.script;
|
|
in
|
|
lib.hasInfix "install -m 0400 /etc/pki/nats-auth.pem " s
|
|
&& lib.hasInfix "install -m 0400 /etc/pki/nats-auth-key.pem " s
|
|
&& lib.hasInfix "install -m 0400 /dev/null " s
|
|
&& lib.hasInfix "callout-user.seed" s
|
|
&& lib.hasInfix "issuer.seed" s;
|
|
}
|
|
{
|
|
# The responder's OIDC secret has one route, the store, under the
|
|
# responder's own leaf. The copy unit's arms are the control: on a queue
|
|
# host without authelia nothing reads authelia's tree, and nothing
|
|
# copies the secret from any host file.
|
|
name = "a queue host without authelia reads the responder's OIDC secret from the store, and copies none from a host file";
|
|
ok =
|
|
let
|
|
services = natsWithStoreIdentity.systemd.services;
|
|
fetch = services.swarm-bao-nats-auth-oidc;
|
|
copy = services.swarm-nats-auth-secrets.script;
|
|
in
|
|
!natsWithStoreIdentity.services.hyperhive.deploy.authelia.enable
|
|
&& fetch.environment.BAO_CLIENT_CERT == "/etc/pki/nats-auth.pem"
|
|
&& fetch.environment.BAO_CLIENT_KEY == "/etc/pki/nats-auth-key.pem"
|
|
&& lib.hasInfix "bao kv get -field=value secret/swarm/services/swarm-nats/oidc/client" fetch.script
|
|
&& lib.hasInfix "/var/lib/nixos-containers/swarm-nats/var/lib/swarm-nats-auth/oidc-client.secret" fetch.script
|
|
&& lib.hasInfix "refresh_consumer swarm-nats swarm-nats-auth.service" fetch.script
|
|
# An unpublished or empty secret fails the unit, so `storeRetry` retries it.
|
|
&& !(lib.hasInfix "exit 0" fetch.script)
|
|
&& lib.elem "container@swarm-nats.service" fetch.before
|
|
&& !(lib.hasInfix "oidc-client" copy)
|
|
&& !(lib.hasInfix "authelia" copy);
|
|
}
|
|
{
|
|
# No responder leaf is a responder with no OIDC secret, which refuses
|
|
# every client. The unit still renders, and fails naming the options.
|
|
name = "a queue host with no responder leaf renders a secret fetch that fails naming the options";
|
|
ok =
|
|
let
|
|
fetch = natsOldPath.systemd.services.swarm-bao-nats-auth-oidc;
|
|
in
|
|
!(fetch.environment ? BAO_CLIENT_CERT)
|
|
&& lib.hasInfix "deploy.nats.authBaoClientCertFile" fetch.script
|
|
&& lib.hasInfix "exit 1" fetch.script
|
|
&& !(lib.hasInfix "bao login" fetch.script);
|
|
}
|
|
{
|
|
# The control: no identity, no store wiring, and the responder's
|
|
# credentials are exactly the three it cannot run without.
|
|
name = "a responder with no store identity is not pointed at a store";
|
|
ok =
|
|
let
|
|
u = responderOf natsOldPath;
|
|
in
|
|
!(u.environment ? BAO_ADDR)
|
|
&& !(u.environment ? BAO_CLIENT_CERT)
|
|
&& lib.length u.serviceConfig.LoadCredential == 3;
|
|
}
|
|
{
|
|
# On the store's host the fetch logs in under the responder's leaf, so it
|
|
# waits for the unit writing that role.
|
|
name = "on the store's host the responder's OIDC secret fetch uses its leaf and waits for its role";
|
|
ok =
|
|
let
|
|
u = natsOnStoreHost.systemd.services.swarm-bao-nats-auth-oidc;
|
|
in
|
|
u.environment.BAO_CLIENT_CERT == "/var/lib/swarm-bao-pki/nats-auth.pem"
|
|
&& lib.elem "swarm-bao-nats-auth-policy.service" u.after
|
|
&& lib.elem "swarm-bao-nats-auth-policy.service" u.wants;
|
|
}
|
|
{
|
|
# On the store's host the glue pairs the responder with a leaf of its
|
|
# own, never the queue's TLS-issuing one or the hive's.
|
|
name = "on the store's host the responder presents its own leaf";
|
|
ok =
|
|
let
|
|
n = natsOnStoreHost.services.hyperhive.deploy.nats;
|
|
b = natsOnStoreHost.services.hyperhive.deploy.bao;
|
|
in
|
|
n.authBaoClientCertFile == "/var/lib/swarm-bao-pki/nats-auth.pem"
|
|
&& n.authBaoClientKeyFile == "/var/lib/swarm-bao-pki/nats-auth-key.pem"
|
|
&& n.authBaoClientCertFile != n.baoClientCertFile
|
|
&& n.authBaoClientCertFile != b.clientCertFile;
|
|
}
|
|
{
|
|
# Not a rename test. `hostClientSecretDir` is `readOnly`, so the fixture
|
|
# cannot define it; what can break is a reader left pointing at
|
|
# `swarm.authelia` instead of `deploy.authelia`. Several modules read this
|
|
# through an `autheliaCfg` alias, where a path-shaped grep does not see it.
|
|
name = "a consumer of authelia's host client-secret dir renders it from the deploy namespace";
|
|
ok = lib.hasPrefix "/var/lib/nixos-containers/swarm-authelia/var/lib/authelia-swarm/oidc-clients/" autheliaOldPath.services.hyperhive.otel.clientSecretFile;
|
|
}
|
|
{
|
|
# authelia exits at startup on `users: {}`, so an empty database, and a
|
|
# file holding exactly that document, must be replaced by the disabled
|
|
# placeholder rather than left for authelia to read.
|
|
name = "an empty authelia users database is seeded with the disabled placeholder";
|
|
ok =
|
|
let
|
|
s =
|
|
autheliaColocated.containers.swarm-authelia.config.systemd.services.authelia-swarm-secrets.script;
|
|
in
|
|
lib.hasInfix ''if [ ! -s "$users" ] || [ "$(cat "$users")" = "users: {}" ]; then'' s
|
|
&& lib.hasInfix "-authelia-users-placeholder.yml \"$users\"" s
|
|
&& !(lib.hasInfix ''echo "users: {}"'' s);
|
|
}
|
|
{
|
|
# The bridge refuses every client but the one named here, so this has
|
|
# to be the client the controller authenticates as. A drift is a
|
|
# controller whose every bridge call is refused.
|
|
name = "the authelia bridge answers the controller's client only";
|
|
ok =
|
|
let
|
|
env =
|
|
autheliaColocated.containers.swarm-authelia.config.systemd.services.swarm-authelia-bridge.environment;
|
|
in
|
|
env.SWARM_AUTHELIA_BRIDGE_CONTROLLER_CLIENT_ID
|
|
== autheliaColocated.services.hyperhive.swarm.controller.queueClientId
|
|
&& env.SWARM_AUTHELIA_BRIDGE_OPERATOR_GROUP == "admins";
|
|
}
|
|
{
|
|
# One address from every host, and a location the IdP host serves: a
|
|
# controller off the authelia host has no other way to the bridge.
|
|
name = "the auth bridge address is the authelia vhost's /bridge/ on every host";
|
|
ok =
|
|
let
|
|
url = h: h.services.hyperhive.deploy.swarm-controller.authBridgeUrl;
|
|
authelia = autheliaColocated.services.hyperhive.swarm.authelia;
|
|
location = autheliaColocated.services.nginx.virtualHosts.${authelia.domain}.locations."/bridge/";
|
|
in
|
|
url autheliaNotColocated == "${autheliaNotColocated.services.hyperhive.swarm.authelia.url}/bridge"
|
|
&& url autheliaColocated == url autheliaNotColocated
|
|
&& location.proxyPass == "http://127.0.0.1:${toString authelia.bridgePort}/";
|
|
}
|
|
{
|
|
# Config NAMES the IdP; it never computes where the IdP is. Both arms
|
|
# matter together: the address is the swarm's name on a hive that runs
|
|
# nothing, and it is the SAME string on the hive that serves the vhost —
|
|
# so a re-introduced co-location branch shows up as the two disagreeing
|
|
# rather than as a value that merely looks plausible on one of them.
|
|
name = "the swarm IdP address is its domain on every hive, co-located or not";
|
|
ok =
|
|
autheliaNotColocated.services.hyperhive.swarm.authelia.url
|
|
== "https://${autheliaNotColocated.services.hyperhive.swarm.authelia.domain}"
|
|
&&
|
|
autheliaColocated.services.hyperhive.swarm.authelia.url
|
|
== autheliaNotColocated.services.hyperhive.swarm.authelia.url;
|
|
}
|
|
{
|
|
# …and the name it resolves through is a DOMAIN, not a host this config
|
|
# picked. `127.0.0.1` is what a co-location-derived default renders on
|
|
# the IdP host, the bridge address is what the queue's defaults render,
|
|
# and neither is an address a client may be handed: the domain is
|
|
# allowed to resolve differently in different places, which is the
|
|
# whole property.
|
|
name = "the swarm IdP address names no host address";
|
|
ok =
|
|
let
|
|
url = autheliaNotColocated.services.hyperhive.swarm.authelia.url;
|
|
bridgeIp = autheliaNotColocated.services.hyperhive.network.bridgeIp;
|
|
in
|
|
!(lib.hasInfix "127.0.0.1" url) && !(lib.hasInfix "localhost" url) && !(lib.hasInfix bridgeIp url);
|
|
}
|
|
];
|
|
in
|
|
runGroup "nats-authelia" cases
|