Watch
0
0
Fork
You've already forked hyperhive
0

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.
This commit is contained in:
atlas 2026-10-30 03:25:57 +01:00 • committed by mara
commit d30cb7d122
6 changed files with 1086 additions and 486 deletions

View file

@ -418,28 +418,31 @@ let
# The whole point of a second principal. The two prefixes and one leaf it
# publishes to and not `swarm/`, so it cannot touch an agent's
# credentials or the appservice token beside the controller's client;
# and `read` on six exact leaves alone: the client secret of the journal forwarder
# that shares its identity in authelia's container, and authelia's five keys
# (the next case), so a unit whose job is copying a file cannot recover
# what is already there. Pinned as the full capability list per path,
# because an added capability is exactly what a presence check misses.
# The forwarder's `read` is found at its leaf, and the six together are
# counted, so a read on any other path fails the count.
name = "the publisher's grant writes the hive and service prefixes and the controller's client, and reads its forwarder's client and authelia's keys alone";
# and `read` on the `oidc/client` leaves it mints alone (the controller's
# exactly, each hive's and each service's by one `+` segment) and
# authelia's five keys (the next case), so a unit whose job is minting
# cannot recover what is already there beyond its own secrets. Pinned as
# the full capability list per path, because an added capability is
# exactly what a presence check misses. The `oidc/client` stanzas repeat
# `create` and `update`, since the narrower path replaces the prefix's
# capabilities. The eight reads are counted, so a read on any other path
# fails the count.
name = "the publisher's grant writes the hive and service prefixes and the controller's client, and reads the oidc client leaves and authelia's keys alone";
ok =
let
s = baoGrantHere.systemd.services.swarm-bao-secret-publisher-policy.script;
in
lib.hasInfix "path \"secret/data/swarm/hives/*\" {\n capabilities = [\"create\", \"update\"]" s
&& lib.hasInfix "path \"secret/data/swarm/services/*\" {\n capabilities = [\"create\", \"update\"]" s
&& lib.hasInfix "path \"secret/data/swarm/controller/swarm-controller/oidc/client\" {\n capabilities = [\"create\", \"update\"]\n}" s
&& lib.hasInfix "path \"secret/data/swarm/controller/swarm-controller/oidc/client\" {\n capabilities = [\"create\", \"update\", \"read\"]\n}" s
&& lib.hasInfix "path \"secret/data/swarm/hives/+/oidc/client\" {\n capabilities = [\"create\", \"update\", \"read\"]\n}" s
&& lib.hasInfix "path \"secret/data/swarm/services/+/oidc/client\" {\n capabilities = [\"create\", \"update\", \"read\"]\n}" s
&& !(lib.hasInfix "secret/data/swarm/controller/*" s)
&& !(lib.hasInfix "appservice-token" s)
&& !(lib.hasInfix "secret/data/swarm/agents" s)
&& !(lib.hasInfix "secret/data/swarm/*" s)
&& !(lib.hasInfix "sys/policies/acl" s)
&& lib.hasInfix "path \"secret/data/swarm/services/swarm-authelia-collector/oidc/client\" {\n capabilities = [\"create\", \"update\", \"read\"]\n}" s
&& lib.length (lib.splitString "\"read\"" s) == 7
&& lib.length (lib.splitString "\"read\"" s) == 9
&& !(lib.hasInfix "\"list\"" s);
}
{

View file

@ -311,7 +311,7 @@ let
ok = lib.hasInfix "--builder-client swarm-builder" natsHere.containers.swarm-nats.config.systemd.services.swarm-nats-auth.serviceConfig.ExecStart;
}
{
name = "the builder's client is registered as a machine, and the publisher carries its secret";
name = "the builder's client is registered as a machine, and the mint stores its secret";
ok =
let
c = lib.findFirst (
@ -321,7 +321,7 @@ let
c != null
&& c.kind == "machine"
&& lib.hasInfix "secret/swarm/services/swarm-builder/oidc/client" (
autheliaHere.containers.swarm-authelia.config.systemd.services.swarm-secret-publish.script
autheliaHere.containers.swarm-authelia.config.systemd.services.authelia-swarm-secrets.script
);
}
{

View file

@ -95,7 +95,7 @@ let
&& !(bare.systemd.services ? swarm-ci-metrics-ca-bundle);
}
{
name = "the poller's client is registered as a machine, and the publisher carries its secret";
name = "the poller's client is registered as a machine, and the mint stores its secret";
ok =
let
c = lib.findFirst (
@ -106,7 +106,7 @@ let
&& c.kind == "machine"
&& c.audience == [ clientId ]
&& lib.hasInfix "secret/swarm/services/${clientId}/oidc/client" (
allLocal.containers.swarm-authelia.config.systemd.services.swarm-secret-publish.script
allLocal.containers.swarm-authelia.config.systemd.services.authelia-swarm-secrets.script
);
}
];

View file

@ -537,7 +537,11 @@ let
]
&& !(lib.hasInfix "client_secret" client)
&& !(lib.hasInfix "mint swarmctl" s)
&& lib.hasInfix "mint swarm-authelia-bridge" s;
&& !(lib.hasInfix "mint_client_secret swarmctl" s)
&& (
lib.hasInfix "mint swarm-authelia-bridge" s
|| lib.hasInfix "mint_client_secret swarm-authelia-bridge" s
);
}
{
# swarm-controller checks no caller itself: this location's authelia

View file

@ -352,14 +352,15 @@ let
{
# The writer and the reader spell the path independently; this is the
# one place that sees both rendered.
name = "the hive collector reads the path the secret publisher writes";
name = "the hive collector reads the path the mint writes";
ok =
let
fetch = builtins.unsafeDiscardStringContext (hiveOtelFetch hiveOtelAllLocal).script;
publish = builtins.unsafeDiscardStringContext hiveOtelAllLocal.containers.swarm-authelia.config.systemd.services.swarm-secret-publish.script;
publish = builtins.unsafeDiscardStringContext hiveOtelAllLocal.containers.swarm-authelia.config.systemd.services.authelia-swarm-secrets.script;
path = "secret/swarm/hives/h1/oidc/client";
in
lib.hasInfix "bao kv get -field=value ${path}" fetch && lib.hasInfix "bao kv put ${path} " publish;
lib.hasInfix "bao kv get -field=value ${path}" fetch
&& lib.hasInfix "mint_client_secret hive-h1 ${path}" publish;
}
{
# The store's units exist only on its host. The PKI unit is wanted, never

View file

@ -24,7 +24,7 @@ let
# 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 publisher walks it — an arm written
# 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;
@ -73,47 +73,45 @@ let
# pair is what makes the reader's absence arm mean anything.
inherit (shared) baoWithMatrix;
# The publisher runs inside authelia's container.
# 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 publisher writes every hive's own client under that hive's prefix";
name = "the mint stores every hive's own client under that hive's prefix";
ok =
let
s = (inAuthelia secretPublisherHere).systemd.services.swarm-secret-publish.script;
s = (inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script;
in
lib.all
(h: lib.hasInfix "secret/swarm/hives/${h}/oidc/client" s && lib.hasInfix "/hive-${h}.secret" s)
[
"h1"
"h2"
];
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 publisher writes the controller's client under controller/, never services/";
name = "the mint stores the controller's client under controller/, never services/";
ok =
let
s = (inAuthelia secretPublisherHere).systemd.services.swarm-secret-publish.script;
s = (inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script;
in
lib.hasInfix "secret/swarm/controller/swarm-controller/oidc/client" s
&& lib.hasInfix "/swarm-controller.secret" 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
# publisher in authelia's container writes the path the reader on
# 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 publisher writes the swarm service path grafana reads";
name = "the mint stores the swarm service path grafana reads";
ok = lib.hasInfix "secret/swarm/services/swarm-grafana/oidc/client" (
(inAuthelia secretPublisherHere).systemd.services.swarm-secret-publish.script
(inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script
);
}
{
@ -180,30 +178,27 @@ let
}
{
# The other half of the queue's, the homeserver's and the forge's one
# route: the publisher on an IdP host that runs none of them carries each
# route: the mint on an IdP host that runs none of them stores each
# secret to the path its reader fetches.
name = "the publisher on an authelia host carries the queue's, homeserver's and forge's secrets";
name = "the mint on an authelia host stores the queue's, homeserver's and forge's secrets";
ok =
let
s = (inAuthelia secretPublisherRemote).systemd.services.swarm-secret-publish.script;
s = (inAuthelia secretPublisherRemote).systemd.services.authelia-swarm-secrets.script;
in
lib.hasInfix "secret/swarm/services/swarm-nats/oidc/client" s
&& lib.hasInfix "/swarm-nats.secret" s
&& lib.hasInfix "secret/swarm/services/tuwunel/oidc/client" s
&& lib.hasInfix "/tuwunel.secret" s
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 "secret/swarm/services/forgejo/oidc/client" s
&& lib.hasInfix "/forgejo.secret" s;
&& 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
# publisher in authelia's container writes the path the reader on 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 publisher writes the swarm service path the collector reads";
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.swarm-secret-publish.script
(inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script
);
}
{
@ -212,22 +207,22 @@ let
# 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 publisher carries the store forwarder's secret too";
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.swarm-secret-publish.script
(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 publisher carries the CI runner, Grafana, log store, cache, forge, homeserver, metrics store and authelia forwarders' secrets";
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.swarm-secret-publish.script
(inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script
)
)
[
@ -244,7 +239,7 @@ let
{
# 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 publisher carries its secret";
name = "the review classifier's client is registered as a machine, and the mint stores its secret";
ok =
let
c = lib.findFirst (
@ -254,15 +249,15 @@ let
c != null
&& c.kind == "machine"
&& lib.hasInfix "secret/swarm/services/swarm-review-classifier/oidc/client" (
(inAuthelia secretPublisherHere).systemd.services.swarm-secret-publish.script
(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 publisher carries its secret
# 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 publisher carries its secret";
name = "the store UI's client is registered interactive at its callback, and the mint stores its secret";
ok =
let
ui = lib.findFirst (
@ -273,7 +268,7 @@ let
&& 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.swarm-secret-publish.script
(inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script
)
&& !(lib.any (
c: c.id == "swarm-bao-ui"
@ -307,42 +302,44 @@ let
&& !bare.services.hyperhive.deploy.swarm-secret-publisher.enable;
}
{
# The one security property of this unit, and why its push cannot be
# rewritten into the obvious shape: `bao` is an external binary, so an
# argument is world-readable in /proc for the life of the call.
# `value=@<path>` hands it the path and bao opens the file itself.
# 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 second arm is what makes the first mean anything — `value=@` can
# sit one line above a command substitution that put the plaintext in
# argv anyway.
# 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 this unit's comments name
# the hazard verbatim so the next editor does not reintroduce it. Without
# the strip this case reads that warning and fails — a check the artifact
# defeats by DESCRIBING the thing it is checked for.
name = "the publisher hands bao the secret's path, never the secret";
# 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
s = (inAuthelia secretPublisherHere).systemd.services.swarm-secret-publish.script;
code = lib.concatStringsSep "\n" (
lib.filter (l: builtins.match "[[:space:]]*#.*" l == null) (lib.splitString "\n" s)
);
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
lib.hasInfix "value=@" code && !(lib.hasInfix "$(cat" code);
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 publisher writes no such path. The
# control: the same script writes each hive's own client.
name = "the publisher writes no hive-shared agent queue credential";
# 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.swarm-secret-publish.script;
s = (inAuthelia secretPublisherHere).systemd.services.authelia-swarm-secrets.script;
in
!(lib.hasInfix "/queue/agent" s)
&& !(lib.hasInfix "-agent.secret" s)
&& lib.hasInfix "secret/swarm/hives/h1/oidc/client" 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
@ -371,7 +368,10 @@ let
&& lib.hasInfix "-token-only" c
&& !(lib.hasInfix "bao kv" (lib.head (lib.splitString "bao login" c)));
in
holdsTokenFirst (inAuthelia secretPublisherHere).systemd.services.swarm-secret-publish.script
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
@ -389,40 +389,109 @@ let
# 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 = "a publisher holding an identity runs in authelia's container on a host with no store";
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 ? swarm-secret-publish)
&& (c.systemd.paths ? swarm-secret-publish)
&& !(m.systemd.services ? swarm-secret-publish)
&& !(m.systemd.paths ? swarm-secret-publish)
&& (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. Minting the secrets is not being
# able to publish them: with no certificate the unit would fail a TLS
# handshake on every rotation, so it must not exist at all, and nspawn
# gets no credential to load.
name = "an IdP host with no store identity renders no publisher";
# 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 ? swarm-secret-publish)
&& !(c.systemd.paths ? swarm-secret-publish)
&& !(m.systemd.services ? swarm-secret-publish)
&& !(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 both.
&& ((inAuthelia secretPublisherHere).systemd.services ? swarm-secret-publish)
# 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
@ -453,37 +522,17 @@ let
&& !(onDisk.environment ? BAO_ADDR)
&& !(onDisk.serviceConfig ? LoadCredential);
}
{
# The path unit's start is a no-op while its target is `active (exited)`,
# so a secret minted after the first run would never be published.
# `Type` is read too, so a missing `serviceConfig` cannot pass the arm.
# The watched directory is the mint's own, inside the container.
name = "the publisher's path unit watches the mint's directory and starts a oneshot that does not remain active";
ok =
let
c = inAuthelia secretPublisherHere;
sc = c.systemd.services.swarm-secret-publish.serviceConfig;
in
c.systemd.paths.swarm-secret-publish.pathConfig.Unit == "swarm-secret-publish.service"
&&
c.systemd.paths.swarm-secret-publish.pathConfig.PathChanged
== "/var/lib/authelia-swarm/oidc-clients"
&& lib.hasInfix "/var/lib/authelia-swarm/oidc-clients/swarm-grafana.secret" c.systemd.services.swarm-secret-publish.script
&& !(lib.hasInfix "/var/lib/nixos-containers" c.systemd.services.swarm-secret-publish.script)
&& sc.Type == "oneshot"
&& !(sc.RemainAfterExit or false);
}
{
# 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 publisher's store identity reaches authelia's container as system credentials";
name = "the store identity reaches authelia's container as system credentials";
ok =
let
dir = "/run/credentials/swarm-secret-publish.service";
unit = m: (inAuthelia m).systemd.services.swarm-secret-publish;
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
@ -518,15 +567,15 @@ let
}
{
# 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 publisher, and the
# publisher may not order itself in front of authelia.
name = "authelia does not wait for the secret publisher";
# 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.swarm-secret-publish;
unit = "swarm-secret-publish.service";
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"