Docs (review on #4948): the harness's restart settings are not useful to a reader, so the `Restart`/`RestartSec` bullet in docs/turn-loop and the "30 seconds later, no start limit" wording in docs/swarm are removed. What stays is the behaviour: the harness exits and comes back until the queue accepts it. Grant: with the harness's shared-client fallback gone, nothing presents the hive's shared agent client (`hive-<hive>-agent`) to the queue; swarm-logs uses that OIDC client over HTTP only. swarm-nats-auth now refuses it outright instead of granting the configured `$SWARM.term.{hive}.>` / `$SWARM.agent-state.{hive}.>` publish and its `_INBOX.client.<id>.>` subscribe, so an agent holding it can no longer publish as a sibling. The agent arm still runs before the hive arm, so that id never falls through to its hive's grant. Removed with it: the `--agent-publish-subject` flag and its two uses in swarm-nats.nix, `Policy`'s agent subject list, and swarm-queue-client's `client_inbox_prefix`. The module-eval now asserts the flag is absent; the policy tests assert the refusal and that the hive itself is still admitted.
300 lines
14 KiB
Nix
300 lines
14 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 repointed to the new namespace still renders the derived
|
|
# path, which is the failure this slice could actually have shipped — seven
|
|
# of those reads went 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: it is the shape whose IdP address used to be null, and
|
|
# before that a co-location-derived guess.
|
|
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, which would deny every
|
|
# client: it is delivered empty, and the three credentials the responder
|
|
# cannot run without are delivered too.
|
|
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 "oidc-client.secret" s;
|
|
}
|
|
{
|
|
# 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 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 the
|
|
# namespace it moved out of. Five 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.hasInfix "/var/lib/nixos-containers/swarm-authelia/var/lib/authelia-swarm/oidc-clients/" autheliaOldPath.systemd.services.swarm-nats-auth-secrets.script;
|
|
}
|
|
{
|
|
# 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 the old default rendered on the IdP host,
|
|
# the bridge address is what the queue's defaults still 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
|