Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/module-eval/secret-publisher.nix
atlas d30cb7d122 keep authelia's oidc client secrets in the store, not on disk
The mint creates each client's secret in the swarm secret store and writes only the digest to disk. It mints only when the store answers and holds none. A new unit moves the plaintext files an earlier release left into the store and removes each once the store holds the same value. The bridge reads its secret from the store into /run before each start. The secret publisher unit and its path unit are gone.
2026-10-30 16:57:58 +01:00

597 lines
28 KiB
Nix

# `checks.module-eval-secret-publisher` — see ./lib.nix for the shared
# rationale (why this suite exists, naming convention, "evaluates
# not executes").
{
pkgs,
lib,
self,
nixosSystem,
shared,
}:
let
inherit
(import ./lib.nix {
inherit
pkgs
lib
self
nixosSystem
;
})
hive
runGroup
;
# The IdP and the store on one machine: the shape where minted plaintext and
# a store identity are both present without an operator placing anything.
# Two hives in the roster, because the mint walks it — an arm written
# against a single-hive fixture passes on a hardcoded name.
secretPublisherHere = hive {
deploy.bao.enable = true;
deploy.authelia.enable = true;
swarm.hives.h2.domain = "h2.t.local";
};
# The IdP with no store on the box and a leaf placed by hand, which is the
# deployment this unit exists for: authelia is the one host the store is
# guaranteed not to share once either has a machine of its own.
#
# ⚠️ `enable` is deliberately NOT set here: leaving it unset is what makes
# this fixture exercise the default rather than mask it.
secretPublisherRemote = hive {
deploy.authelia.enable = true;
deploy.swarm-secret-publisher.baoClientCertFile = "/etc/pki/publisher.pem";
deploy.swarm-secret-publisher.baoClientKeyFile = "/etc/pki/publisher-key.pem";
};
# The same IdP with the identity taken away. Minting the secrets is not being
# able to publish them, and this is the arm that separates the two.
secretPublisherNoIdentity = shared.autheliaOnly;
# Duplicated from grafana.nix — a case here needs it too.
# The same UI with the IdP on ANOTHER host and a store leaf placed by hand.
# Knowing an IdP is not running one: `swarm.authelia.url` is what says this
# swarm has SSO, and nothing about this host does. Identical to the fixture
# above in everything the delivery path reads, which is the point.
grafanaRemoteAuthelia = hive {
deploy.grafana.enable = true;
deploy.grafana.plugins = [ ];
deploy.grafana.package = pkgs.emptyDirectory;
swarm.authelia.url = "https://auth.example.invalid";
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
};
# The same collector with the IdP on ANOTHER host and a store leaf placed by
# hand. Identical to the fixture above in everything the delivery path
# reads, which is the point.
otelBaoRemoteAuthelia = shared.otelReaderMissingLeaf;
inherit (shared) bare;
# The store and a service that reads from it, versus the store alone. The
# pair is what makes the reader's absence arm mean anything.
inherit (shared) baoWithMatrix;
# The mint runs inside authelia's container.
inAuthelia = m: m.containers.${m.services.hyperhive.swarm.authelia.machine}.config;
cases = [
{
# Each hive's hive-c0re reads this exact path
# (`swarm_secret_client::queue::hive_client_path`, pinned by that crate's
# own test), for every hive in the roster, not only the one this host is.
name = "the mint stores every hive's own client under that hive's prefix";
ok =
let
s = (inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script;
in
lib.all (h: lib.hasInfix "mint_client_secret hive-${h} secret/swarm/hives/${h}/oidc/client" s) [
"h1"
"h2"
];
}
{
# The controller reads this exact path
# (`swarm_secret_client::queue::controller_client_path`, pinned by that
# crate's own test). Under `controller/`, which no hive reads: this
# client may read every hive's status.
name = "the mint stores the controller's client under controller/, never services/";
ok =
let
s = (inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script;
in
lib.hasInfix "secret/swarm/controller/swarm-controller/oidc/client" s
&& lib.hasInfix "mint_client_secret swarm-controller secret/swarm/controller/swarm-controller/oidc/client" s
&& !(lib.hasInfix "secret/swarm/services/swarm-controller" s);
}
{
# Both ends of a wire nothing at eval time carries end to end: the
# mint in authelia's container writes the path the reader on
# Grafana's host reads, and the two files agree only because both compose
# it from the same swarm-wide client id.
name = "the mint stores the swarm service path grafana reads";
ok = lib.hasInfix "secret/swarm/services/swarm-grafana/oidc/client" (
(inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script
);
}
{
# Registering the client cannot live where the rest of grafana's module
# lives: that block is gated on this host RUNNING grafana, so on the split
# deployment nothing registered the client, authelia minted no secret, and
# every layer below had nothing to carry. The second arm is the control —
# a host with no IdP registers nothing.
name = "the swarm's grafana client is registered wherever authelia runs";
ok =
let
clients = m: map (c: c.id) m.services.hyperhive.swarm.authelia.oidc.clients;
in
lib.elem "swarm-grafana" (clients secretPublisherHere)
&& !(lib.elem "swarm-grafana" (clients grafanaRemoteAuthelia));
}
{
# The collector's half of the same property: the client is not gated on
# `deploy.swarm-otel.enable`, which would let a split deployment register
# it nowhere, so authelia would mint nothing to publish.
name = "the swarm's collector client is registered wherever authelia runs";
ok =
let
clients = m: map (c: c.id) m.services.hyperhive.swarm.authelia.oidc.clients;
in
lib.elem "swarm-collector" (clients secretPublisherHere)
&& !(lib.elem "swarm-collector" (clients otelBaoRemoteAuthelia));
}
{
# Registration follows authelia, not the service: the IdP host below runs
# no controller, queue or homeserver (the first arms), and still lists all
# three clients. The last arm is the control — a host with no IdP lists
# none of them.
name = "an authelia host running no controller, queue or homeserver registers all three clients";
ok =
let
d = secretPublisherRemote.services.hyperhive.deploy;
clients = m: map (c: c.id) m.services.hyperhive.swarm.authelia.oidc.clients;
byId =
id:
lib.findFirst (
c: c.id == id
) null secretPublisherRemote.services.hyperhive.swarm.authelia.oidc.clients;
controller = byId "swarm-controller";
nats = byId "swarm-nats";
matrix = byId "tuwunel";
in
!d.swarm-controller.enable
&& !d.nats.enable
&& !d.matrix.enable
&& controller.kind == "machine"
&& controller.audience == [ "swarm-controller" ]
&& nats.kind == "machine"
&&
matrix.redirectUris == [
"https://chat.t.local/_matrix/client/unstable/login/sso/callback/tuwunel"
]
&& matrix.tokenEndpointAuthMethod == "client_secret_post"
&& !(lib.any (id: lib.elem id (clients grafanaRemoteAuthelia)) [
"swarm-controller"
"swarm-nats"
"tuwunel"
]);
}
{
# The other half of the queue's, the homeserver's and the forge's one
# route: the mint on an IdP host that runs none of them stores each
# secret to the path its reader fetches.
name = "the mint on an authelia host stores the queue's, homeserver's and forge's secrets";
ok =
let
s = (inAuthelia secretPublisherRemote).systemd.services.authelia-swarm-secrets.script;
in
lib.hasInfix "mint_client_secret swarm-nats secret/swarm/services/swarm-nats/oidc/client" s
&& lib.hasInfix "mint_client_secret tuwunel secret/swarm/services/tuwunel/oidc/client" s
&& !secretPublisherRemote.services.hyperhive.deploy.forgejo.enable
&& lib.hasInfix "mint_client_secret forgejo secret/swarm/services/forgejo/oidc/client" s;
}
{
# Both ends of a wire nothing at eval time carries end to end: the
# mint in authelia's container writes the path the reader on the
# collector's host reads, and the two files agree only because both
# compose it from the same swarm-wide client id. `secretPublisherHere`
# already grew this client when `serviceClientIds` did.
name = "the mint stores the swarm service path the collector reads";
ok = lib.hasInfix "secret/swarm/services/swarm-collector/oidc/client" (
(inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script
);
}
{
# The third swarm service on that list, and the one whose absence is
# hardest to see: the store's own forwarder holds a client authelia
# registers and mints for, so every layer above looks complete while
# the one hop that CARRIES the value skips it and the reader on the
# store's host waits forever on a path nothing writes.
name = "the mint stores the store forwarder's secret too";
ok = lib.hasInfix "secret/swarm/services/swarm-bao-collector/oidc/client" (
(inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script
);
}
{
# The forwarders in the CI runner's, Grafana's, the log store's, the
# forge's, the homeserver's and authelia's containers read their secrets
# from these paths, and the metrics store's host reads its container's.
name = "the mint stores the CI runner, Grafana, log store, cache, forge, homeserver, metrics store and authelia forwarders' secrets";
ok =
lib.all
(
id:
lib.hasInfix "secret/swarm/services/${id}/oidc/client" (
(inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script
)
)
[
"swarm-ci-runner-collector"
"swarm-grafana-collector"
"swarm-victorialogs-collector"
"swarm-forge-collector"
"swarm-matrix-collector"
"swarm-victoriametrics-collector"
"swarm-authelia-collector"
"swarm-attic-collector"
];
}
{
# The review classifier reads its queue client secret from the store
# inside its container; authelia registers it on every IdP host.
name = "the review classifier's client is registered as a machine, and the mint stores its secret";
ok =
let
c = lib.findFirst (
c: c.id == "swarm-review-classifier"
) null secretPublisherHere.services.hyperhive.swarm.authelia.oidc.clients;
in
c != null
&& c.kind == "machine"
&& lib.hasInfix "secret/swarm/services/swarm-review-classifier/oidc/client" (
(inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script
);
}
{
# The store's UI login: authelia registers the client with the callback
# the store's `oidc` role allows, and the mint stores its secret
# to the path `swarm-bao-operator-viewer-policy` reads. Not registered
# where authelia is not.
name = "the store UI's client is registered interactive at its callback, and the mint stores its secret";
ok =
let
ui = lib.findFirst (
c: c.id == "swarm-bao-ui"
) null secretPublisherHere.services.hyperhive.swarm.authelia.oidc.clients;
in
ui != null
&& ui.kind == "interactive"
&& ui.redirectUris == [ "https://bao-ui.t.local/ui/vault/auth/oidc/oidc/callback" ]
&& lib.hasInfix "secret/swarm/services/swarm-bao-ui/oidc/client" (
(inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script
)
&& !(lib.any (
c: c.id == "swarm-bao-ui"
) grafanaRemoteAuthelia.services.hyperhive.swarm.authelia.oidc.clients);
}
{
# The same hole the controller's case above names, open a second time: the
# PKI script grew a third leaf and no case read it.
name = "the store mints a leaf for the secret publisher, and the publisher is pointed at it";
ok =
let
m = secretPublisherHere;
p = m.services.hyperhive.deploy.swarm-secret-publisher;
in
lib.hasInfix "secret-publisher.pem" m.systemd.services.swarm-bao-pki.script
&& p.baoClientCertFile == "/var/lib/swarm-bao-pki/secret-publisher.pem"
&& p.baoClientKeyFile == "/var/lib/swarm-bao-pki/secret-publisher-key.pem";
}
{
# mara caught this by reading, which means no arm existed for it: the
# default asked `authelia.enable && bao.enable`, so the split deployment
# this unit is FOR defaulted off and published nothing, silently. The
# default is set by ../host-modules/glue-secret-publisher-authelia.nix.
#
# The second clause is the control. Without it this passes on a default
# of plain `true`, which would be a different bug with the same symptom
# — an IdP-less host claiming it publishes secrets it never mints.
name = "the publisher defaults on where secrets are minted, whether or not the store is local";
ok =
secretPublisherRemote.services.hyperhive.deploy.swarm-secret-publisher.enable
&& !bare.services.hyperhive.deploy.swarm-secret-publisher.enable;
}
{
# The one security property of the mint's write: `bao` is an external
# binary, so an argument is world-readable in /proc for the life of the
# call. `value=-` makes bao read the value from stdin.
#
# The arms below the real script are what make it mean anything: a
# `value=-` elsewhere in the text can sit beside a put that passes the
# plaintext as an argument.
#
# ⚠️ Comments are stripped first, and that is not tidiness. A `script`
# renders its own comments into the text, and a comment can name the
# hazard verbatim. Without the strip this case would read that warning
# and fail — a check the artifact defeats by DESCRIBING the thing it is
# checked for.
name = "the mint hands bao the secret on stdin, never as an argument";
ok =
let
code = s: lib.filter (l: builtins.match "[[:space:]]*#.*" l == null) (lib.splitString "\n" s);
puts = s: lib.filter (l: lib.hasInfix "kv put" l) (code s);
onStdin = s: puts s != [ ] && lib.all (l: lib.hasInfix "value=-" l) (puts s);
in
onStdin (inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script
&& onStdin (inAuthelia secretPublisherHere).systemd.services.authelia-swarm-client-secrets.script
&& !(onStdin "printf '%s' \"$secret\" | bao kv put -cas=0 x value=\"$secret\"")
&& !(onStdin "# kv put x value=-\nbao kv put x value=@/tmp/f")
&& onStdin "printf '%s' \"$secret\" | bao kv put -cas=0 x value=-";
}
{
# No hive holds a shared queue credential for its agents, so authelia
# mints no agent client and the mint stores no such path. The
# control: the same script stores each hive's own client.
name = "the mint stores no hive-shared agent queue credential";
ok =
let
s = (inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script;
in
!(lib.hasInfix "/queue/agent" s)
&& !(lib.hasInfix "hive-h1-agent" s)
&& lib.hasInfix "mint_client_secret hive-h1 secret/swarm/hives/h1/oidc/client" s;
}
{
# A property of the SET, not of one unit: both of these authenticate by
# certificate, and `BAO_CLIENT_CERT` is transport rather than identity, so
# a script that reaches `bao kv` without a token asks a token helper this
# host does not carry and fails before the store ever answers. `-token-only`
# is what keeps the token off the helper on the way back out.
#
# Ordering, not presence: the login has to come first, so the check is
# that nothing before it is a data command. Comments are stripped because
# both units explain this in prose directly above the code.
name = "the cert-identity bao units log in before their first read or write, and keep the token out of the helper";
ok =
let
code =
s:
lib.concatStringsSep "\n" (
lib.filter (l: builtins.match "[[:space:]]*#.*" l == null) (lib.splitString "\n" s)
);
holdsTokenFirst =
s:
let
c = code s;
in
lib.hasInfix "bao login" c
&& lib.hasInfix "-token-only" c
&& !(lib.hasInfix "bao kv" (lib.head (lib.splitString "bao login" c)));
in
holdsTokenFirst (inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script
&& holdsTokenFirst (inAuthelia secretPublisherHere)
.systemd.services.authelia-swarm-client-secrets.script
&& holdsTokenFirst (inAuthelia secretPublisherHere).systemd.services.swarm-authelia-bridge.preStart
&& holdsTokenFirst baoWithMatrix.systemd.services.swarm-bao-matrix-oidc.script
# Controls, so a clean verdict above means something. In order: a bare
# read is refused, a read placed before the login is refused, and a
# login that exists only in a comment is refused — that last one is the
# arm the comment-stripping exists for.
&& !(holdsTokenFirst "bao kv get -field=value secret/x")
&& !(holdsTokenFirst "bao kv get secret/x\nBAO_TOKEN=\"$(bao login -method=cert -token-only)\"")
&& !(holdsTokenFirst "# bao login -method=cert -token-only goes here\nbao kv get secret/x")
&& holdsTokenFirst "BAO_TOKEN=\"$(bao login -method=cert -token-only)\"\nbao kv get secret/x";
}
{
# The doctrine three glue files state, as a property a rewrite has to
# keep: a client is defined by holding a certificate the store accepts,
# never by standing next to the store. Gating this on `deploy.bao.enable`
# would have left the unit rendering only on the one deployment that has
# no use for it. It runs beside the mint, in authelia's container, and
# never on the host.
name = "the client secret migration holding an identity runs in authelia's container on a host with no store";
ok =
let
m = secretPublisherRemote;
c = inAuthelia m;
in
!m.services.hyperhive.deploy.bao.enable
&& (c.systemd.services ? authelia-swarm-client-secrets)
&& !(m.systemd.services ? authelia-swarm-client-secrets)
# Control: the host's own unit set is the one read above.
&& (m.systemd.services ? "container@swarm-authelia");
}
{
# What makes the arm above able to fail. Without a certificate the
# plaintext has no store to go to, so the mint keeps it in a file beside
# the digest as before, the migration does not exist, the bridge reads
# that file, and nspawn gets no credential to load.
name = "an IdP host with no store identity keeps the client secrets in files and renders no migration";
ok =
let
m = secretPublisherNoIdentity;
c = inAuthelia m;
mint = c.systemd.services.authelia-swarm-secrets.script;
in
m.services.hyperhive.deploy.authelia.enable
&& !(c.systemd.services ? authelia-swarm-client-secrets)
&& lib.hasInfix "mint() {" mint
&& !(lib.hasInfix "mint_client_secret" mint)
&&
c.systemd.services.swarm-authelia-bridge.environment.SWARM_AUTHELIA_BRIDGE_CLIENT_SECRET_FILE
== "/var/lib/authelia-swarm/oidc-clients/swarm-authelia-bridge.secret"
&& !(c.systemd.services.swarm-authelia-bridge.serviceConfig ? RuntimeDirectory)
&& !(lib.any (lib.hasPrefix "--load-credential=") m.containers.swarm-authelia.extraFlags)
# Controls: with an identity, the same lookups find the other shape.
&& ((inAuthelia secretPublisherHere).systemd.services ? authelia-swarm-client-secrets)
&& lib.hasInfix "mint_client_secret" (
(inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script
)
&& lib.any (lib.hasPrefix "--load-credential=") secretPublisherHere.containers.swarm-authelia.extraFlags;
}
{
# With a store identity no unit writes a plaintext client secret under
# /var/lib: the mint keeps no `$sec` file, no redirect in the container
# writes one, and the bridge reads its secret from a file in /run that it
# fetches from the store before each start. The no-identity host is the
# control: the same lookups find the write there.
name = "no unit writes a plaintext oidc client secret to disk, and the bridge's source is the store";
ok =
let
writesPlaintext =
m:
lib.any (u: lib.hasInfix "> \"$sec\"" (u.script or "")) (
lib.attrValues (inAuthelia m).systemd.services
);
here = inAuthelia secretPublisherHere;
bridge = here.systemd.services.swarm-authelia-bridge;
leaf = "secret/swarm/services/swarm-authelia-bridge/oidc/client";
in
!(writesPlaintext secretPublisherHere)
&& writesPlaintext secretPublisherNoIdentity
&&
bridge.environment.SWARM_AUTHELIA_BRIDGE_CLIENT_SECRET_FILE
== "/run/swarm-authelia-bridge/client-secret"
&& bridge.serviceConfig.RuntimeDirectory == "swarm-authelia-bridge"
&& lib.hasInfix "client_secret_read ${leaf}" bridge.preStart
&& lib.hasInfix "mint_client_secret swarm-authelia-bridge ${leaf}" here.systemd.services.authelia-swarm-secrets.script
&& lib.elem "publisher-bao-cert" bridge.serviceConfig.LoadCredential
&& bridge.serviceConfig.Restart == "on-failure"
# The file it reads from is the one the earlier release left, until the
# migration moves it into the store.
&& lib.hasInfix "legacy=/var/lib/authelia-swarm/oidc-clients/swarm-authelia-bridge.secret" bridge.preStart;
}
{
# Every non-native client is migrated and minted at the leaf its reader
# fetches, not only the ones with a reader in the publisher's list: the
# bridge's own, which only the bridge reads, is the one that list lacks.
name = "the migration visits every client at the leaf the mint creates, and a failure that no retry changes is not retried";
ok =
let
c = inAuthelia secretPublisherHere;
migrate = c.systemd.services.authelia-swarm-client-secrets;
mint = c.systemd.services.authelia-swarm-secrets;
pairs = [
"hive-h1 secret/swarm/hives/h1/oidc/client"
"hive-h2 secret/swarm/hives/h2/oidc/client"
"swarm-grafana secret/swarm/services/swarm-grafana/oidc/client"
"swarm-controller secret/swarm/controller/swarm-controller/oidc/client"
"swarm-authelia-bridge secret/swarm/services/swarm-authelia-bridge/oidc/client"
];
in
lib.all (
p:
lib.hasInfix "migrate_client_secret ${p}" migrate.script
&& lib.hasInfix "mint_client_secret ${p}" mint.script
) pairs
# A native client is public: it has no secret to mint or move.
&& !(lib.hasInfix "swarmctl" migrate.script)
&& !(lib.hasInfix "mint_client_secret swarmctl" mint.script)
&& migrate.serviceConfig.RestartPreventExitStatus == 3
&& mint.serviceConfig.RestartPreventExitStatus == 3
&& migrate.serviceConfig.Restart == "on-failure"
&& migrate.serviceConfig.Type == "oneshot";
}
{
# Every key authelia signs or encrypts with comes from the store, seeded
# from the file it was generated into, and lands at the path the config
# already names. The storage-encryption key encrypts authelia's database,
# so a value that moved would leave it unreadable. With no identity the
# unit still generates the files, which is the control for each absence:
# the generator and the store sequence are found by the same lookups.
name = "authelia's keys are read from the store with the publisher's identity, and generated on disk without one";
ok =
let
unit = m: (inAuthelia m).systemd.services.authelia-swarm-secrets;
viaStore = unit secretPublisherHere;
onDisk = unit secretPublisherNoIdentity;
in
lib.hasInfix "store_seeded secret/swarm/services/swarm-authelia/keys/\"$name\" \"$p\" \"$@\"" viaStore.script
&& lib.hasInfix "p=/var/lib/authelia-swarm/\"$name\".key" viaStore.script
&& lib.hasInfix "keep_key oidc-issuer mint_issuer" viaStore.script
&& lib.hasInfix "for f in jwt session storage-encryption oidc-hmac; do\n keep_key \"$f\" mint_hex" viaStore.script
&& !(lib.hasInfix "genrsa -out" viaStore.script)
&&
viaStore.environment.BAO_CLIENT_CERT
== "/run/credentials/authelia-swarm-secrets.service/publisher-bao-cert"
&& lib.elem "publisher-bao-cert" viaStore.serviceConfig.LoadCredential
&& viaStore.serviceConfig.Restart == "on-failure"
&& !(lib.hasInfix "store_seeded" onDisk.script)
&& lib.hasInfix "genrsa -out" onDisk.script
&& lib.hasInfix "if [ ! -s \"$p\" ]; then" onDisk.script
&& !(onDisk.environment ? BAO_ADDR)
&& !(onDisk.serviceConfig ? LoadCredential);
}
{
# The leaf crosses into the container as nspawn system credentials, and
# the unit loads them and names them in its environment. On the store's
# host the container waits for the unit that mints the leaf, since nspawn
# refuses a missing credential source; with the leaf placed by hand,
# those paths cross.
name = "the store identity reaches authelia's container as system credentials";
ok =
let
dir = "/run/credentials/authelia-swarm-client-secrets.service";
unit = m: (inAuthelia m).systemd.services.authelia-swarm-client-secrets;
flags = m: m.containers.swarm-authelia.extraFlags;
here = unit secretPublisherHere;
in
lib.elem "--load-credential=publisher-bao-cert:/var/lib/swarm-bao-pki/secret-publisher.pem" (
flags secretPublisherHere
)
&& lib.elem "--load-credential=publisher-bao-key:/var/lib/swarm-bao-pki/secret-publisher-key.pem" (
flags secretPublisherHere
)
&& lib.elem "--load-credential=publisher-bao-server-ca:/var/lib/swarm-bao-pki/ca.pem" (
flags secretPublisherHere
)
&&
lib.sort lib.lessThan here.serviceConfig.LoadCredential == [
"publisher-bao-cert"
"publisher-bao-key"
"publisher-bao-server-ca"
]
&& here.environment.BAO_CLIENT_CERT == "${dir}/publisher-bao-cert"
&& here.environment.BAO_CLIENT_KEY == "${dir}/publisher-bao-key"
&& here.environment.BAO_CACERT == "${dir}/publisher-bao-server-ca"
&&
lib.elem "swarm-bao-pki.service"
secretPublisherHere.systemd.services."container@swarm-authelia".after
&& lib.elem "--load-credential=publisher-bao-cert:/etc/pki/publisher.pem" (
flags secretPublisherRemote
)
&& lib.elem "--load-credential=publisher-bao-key:/etc/pki/publisher-key.pem" (
flags secretPublisherRemote
)
&& (unit secretPublisherRemote).environment.BAO_CLIENT_CERT == "${dir}/publisher-bao-cert";
}
{
# Authelia is the swarm's IdP and has to come up while the store is down.
# Nothing it starts after or needs may be the migration, and the
# migration may not order itself in front of authelia.
name = "authelia does not wait for the client secret migration";
ok =
let
c = inAuthelia secretPublisherHere;
authelia = c.systemd.services.authelia-swarm;
publisher = c.systemd.services.authelia-swarm-client-secrets;
unit = "authelia-swarm-client-secrets.service";
in
!(lib.any (l: lib.elem unit (authelia.${l} or [ ])) [
"after"
"requires"
"wants"
"bindsTo"
"requisite"
])
&& !(lib.any (l: lib.elem "authelia-swarm.service" (publisher.${l} or [ ])) [
"before"
"requiredBy"
])
# Controls: both lookups find the real units.
&& authelia.serviceConfig ? ExecStart
&& lib.elem "authelia-swarm-secrets.service" publisher.after;
}
];
in
runGroup "secret-publisher" cases