A client secret has two holders in two containers: the relying party authenticates with the plaintext, authelia compares a digest. Neither side can generate it alone, and `settings` is rendered into the world-readable nix store, so the value cannot be declared. So it is minted here, once, as two files — `<id>.secret` and `<id>.digest`. That split is what lets `oidc-clients.yml` be re-rendered on *every* boot from the nix-declared metadata plus the stored digest: a new redirect URI or a renamed client takes effect on rebuild without rotating a credential another container is already holding. `authelia crypto hash generate pbkdf2 --random` generates the password itself and prints it beside its digest, so no plaintext is ever handed to a second process on a command line. The clients file reaches authelia through `settingsFiles`, which upstream merges at runtime — the same mechanism it already uses for the issuer JWK. Minting fails closed: an empty secret or digest aborts the unit, and the unit is `requiredBy` authelia, so the provider refuses to start rather than serving a client that can never authenticate. That failure would otherwise surface three layers away as an opaque 401 from the token endpoint.
552 lines
24 KiB
Nix
552 lines
24 KiB
Nix
# The swarm's SSO provider: one authelia for the whole swarm, in a
|
||
# `swarm-authelia` nixos-container.
|
||
#
|
||
# Two halves, and only one of them is conditional:
|
||
#
|
||
# - the CLIENT pointer (`url`) exists on every hive, because a hive
|
||
# that doesn't run authelia still has to know where to send people.
|
||
# - the CONTAINER only exists where the swarm's shared services live.
|
||
# `swarm.enableRequiredServices` asserts this module's `enable`
|
||
# (see ./swarm-required-services.nix); a hive is a client by default.
|
||
#
|
||
# Operator and agents are both subjects of the same provider,
|
||
# differentiated by roles/claims rather than by mechanism — there is one
|
||
# IdP and one auth path. The users store is therefore written by a
|
||
# program (swarm-controller), not maintained by hand: agents are created
|
||
# and destroyed continuously, so the subject set is *dynamic*. That is
|
||
# also why the file backend is the right one here and not a placeholder
|
||
# for LDAP: what makes a directory necessary is the size of the subject
|
||
# set, and this deployment's is bounded by one swarm.
|
||
#
|
||
# Two roles, and only the first is unconditional: this is a **session**
|
||
# provider (`auth_request`) always, and an **OIDC** provider when
|
||
# `oidc.clients` is non-empty. The second is derived from the client list
|
||
# instead of carrying its own flag, because authelia refuses to start
|
||
# with a provider that has no clients — a separate `enable` would be a
|
||
# second fact that can disagree with the first.
|
||
#
|
||
# Per-service integration — putting authelia's `auth_request` in front
|
||
# of the gateway's existing `auth_basic` locations — is deliberately NOT
|
||
# here. Standing an SSO provider up is reversible; cutting every
|
||
# operator-facing vhost over to it is not.
|
||
{
|
||
pkgs,
|
||
lib,
|
||
config,
|
||
...
|
||
}:
|
||
let
|
||
cfg = config.services.hyperhive.swarm.authelia;
|
||
hyperhiveCfg = config.services.hyperhive;
|
||
hyperhiveDomain = hyperhiveCfg.domain;
|
||
swarmDomain = hyperhiveCfg.swarm.domain;
|
||
|
||
# Upstream's `services.authelia.instances.<name>` derives the unit,
|
||
# user, group and StateDirectory from the instance name
|
||
# (`authelia` + `-<name>`). Naming them here rather than repeating the
|
||
# literal keeps the generator unit below and the module in step.
|
||
instance = "swarm";
|
||
unitName = "authelia-${instance}";
|
||
stateDir = "/var/lib/${unitName}";
|
||
|
||
# The SWARM's domain, because that is where the protected apps now live
|
||
# (`forge.<swarm>`, `chat.<swarm>`, `auth.<swarm>`). It moves in the
|
||
# same commit as `domain` below and cannot lag it: authelia validates
|
||
# `authelia_url ⊂ cookie domain` at STARTUP, so a half-move does not
|
||
# misbehave at login — it refuses to boot.
|
||
#
|
||
# Total on a null swarm domain for the same reason the option defaults
|
||
# below are: the required-domain assertion in hive-network.nix should
|
||
# be what an operator sees, not a coercion error from here.
|
||
cookieDomain = if swarmDomain == null then "invalid" else swarmDomain;
|
||
|
||
# authelia refuses to start with an OIDC provider that has no clients,
|
||
# so the provider is derived from the client list rather than carrying
|
||
# its own `enable`: one fact, and it cannot contradict itself. An empty
|
||
# list is the default, which makes every hive that has not opted in
|
||
# byte-identical to before.
|
||
oidcEnabled = cfg.oidc.clients != [ ];
|
||
|
||
# Secrets that are 64 random bytes of hex and nothing more. The OIDC
|
||
# hmac key joins them; the issuer key does not (see below — it is RSA).
|
||
randomKeys = [
|
||
"jwt"
|
||
"session"
|
||
"storage-encryption"
|
||
]
|
||
++ lib.optional oidcEnabled "oidc-hmac";
|
||
|
||
# Per-client material lives beside the rest of authelia's state, one
|
||
# file per half: the relying party needs the PLAINTEXT, authelia keeps
|
||
# only a DIGEST. Splitting them is what lets the clients file below be
|
||
# re-rendered on every boot from a secret that was minted once.
|
||
clientsDir = "${stateDir}/oidc-clients";
|
||
clientsFile = "${stateDir}/oidc-clients.yml";
|
||
|
||
# Rendered at RUNTIME, not evaluated: the digest is read from disk by
|
||
# the script, so nothing secret ever enters a nix expression (and
|
||
# therefore the store). Everything else here is public metadata that
|
||
# nix is the right place for.
|
||
renderClient = c: ''
|
||
printf -- ' - client_id: %s\n' ${lib.escapeShellArg c.id}
|
||
printf -- ' client_name: %s\n' ${lib.escapeShellArg c.description}
|
||
printf -- " client_secret: '%s'\n" "$(cat ${lib.escapeShellArg "${clientsDir}/${c.id}.digest"})"
|
||
printf -- ' authorization_policy: one_factor\n'
|
||
printf -- ' scopes: [openid, profile, email, groups]\n'
|
||
printf -- ' redirect_uris:\n'
|
||
${lib.concatMapStrings (u: ''
|
||
printf -- ' - %s\n' ${lib.escapeShellArg u}
|
||
'') c.redirectUris}
|
||
'';
|
||
|
||
# The OIDC half of the first-boot generator, kept out of the script
|
||
# body so neither is read through the other's indentation.
|
||
oidcGenScript = lib.optionalString oidcEnabled ''
|
||
# The issuer key is the one secret here that is NOT interchangeable
|
||
# with a random blob: it *signs* id tokens, and every relying party
|
||
# verifies them against the public half served at `/jwks.json`. A
|
||
# symmetric secret cannot do that, so this one is an RSA pair.
|
||
#
|
||
# Rotating it invalidates every token already issued, which is why
|
||
# it is generated once and left alone — same reason as the session
|
||
# and storage keys above.
|
||
issuer=${lib.escapeShellArg stateDir}/oidc-issuer.key
|
||
if [ ! -s "$issuer" ]; then
|
||
openssl genrsa -out "$issuer" 4096
|
||
echo "generated $issuer"
|
||
fi
|
||
chmod 0600 "$issuer"
|
||
|
||
# Each client's secret, minted once and kept as two files: the
|
||
# plaintext its relying party authenticates with, and the digest
|
||
# authelia compares against. The two live in different containers,
|
||
# so neither side can generate it alone — this is the only place
|
||
# that sees both.
|
||
#
|
||
# `--random` is why no plaintext ever reaches an argv: authelia
|
||
# generates the password itself and prints it beside its digest,
|
||
# so nothing has to be handed to a second process on a command
|
||
# line.
|
||
clients=${lib.escapeShellArg clientsDir}
|
||
mkdir -p "$clients"
|
||
chmod 0700 "$clients"
|
||
|
||
mint() {
|
||
sec="$clients/$1.secret"
|
||
dig="$clients/$1.digest"
|
||
if [ -s "$sec" ] && [ -s "$dig" ]; then
|
||
chmod 0600 "$sec" "$dig"
|
||
return
|
||
fi
|
||
out=$(authelia crypto hash generate pbkdf2 --variant sha512 --random)
|
||
printf '%s' "$out" | sed -n 's/^Random Password: *//p' > "$sec"
|
||
printf '%s' "$out" | sed -n 's/^Digest: *//p' > "$dig"
|
||
chmod 0600 "$sec" "$dig"
|
||
# Fail closed. An empty secret is a client that can never
|
||
# authenticate, and it surfaces three layers away as an opaque
|
||
# 401 from the token endpoint — refusing to start is by far the
|
||
# cheaper failure to diagnose.
|
||
if [ ! -s "$sec" ] || [ ! -s "$dig" ]; then
|
||
echo "authelia crypto hash generate produced no secret/digest for $1" >&2
|
||
exit 1
|
||
fi
|
||
echo "minted client secret for $1"
|
||
}
|
||
|
||
${lib.concatMapStrings (c: ''
|
||
mint ${lib.escapeShellArg c.id}
|
||
'') cfg.oidc.clients}
|
||
|
||
# Re-rendered every boot, deliberately: the secret is minted once,
|
||
# but the metadata around it (a new redirect URI, a renamed client)
|
||
# comes from nix and has to be able to change without disturbing
|
||
# the secret. Written through a temp file so a crash mid-write
|
||
# cannot leave authelia half a file to parse.
|
||
{
|
||
printf -- 'identity_providers:\n'
|
||
printf -- ' oidc:\n'
|
||
printf -- ' clients:\n'
|
||
${lib.concatMapStrings renderClient cfg.oidc.clients}
|
||
} > ${lib.escapeShellArg "${clientsFile}.tmp"}
|
||
chmod 0600 ${lib.escapeShellArg "${clientsFile}.tmp"}
|
||
mv ${lib.escapeShellArg "${clientsFile}.tmp"} ${lib.escapeShellArg clientsFile}
|
||
'';
|
||
in
|
||
{
|
||
options.services.hyperhive.swarm.authelia = {
|
||
enable = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = false;
|
||
example = true;
|
||
description = ''
|
||
Run the swarm's authelia in a `swarm-authelia` container on this
|
||
host. `services.hyperhive.swarm.enableRequiredServices` turns
|
||
this on — a swarm has one SSO provider, and that says it lives
|
||
here.
|
||
|
||
With it off, this hive is a *client*: `url` below still points
|
||
at whoever runs it, and no container is created.
|
||
'';
|
||
};
|
||
|
||
package = lib.mkOption {
|
||
type = lib.types.package;
|
||
default = pkgs.authelia;
|
||
defaultText = lib.literalExpression "pkgs.authelia";
|
||
description = ''
|
||
authelia package to run in the container. Defaults to
|
||
nixpkgs's; override to pin a specific upstream.
|
||
'';
|
||
};
|
||
|
||
port = lib.mkOption {
|
||
type = lib.types.port;
|
||
default = 9091;
|
||
description = ''
|
||
TCP port authelia listens on. 9091 is upstream's default and
|
||
sits outside hyperhive's claimed ranges (dashboard 7000, forge
|
||
3000, matrix 8008, every agent in 8100..8999 via FNV-1a hash).
|
||
'';
|
||
};
|
||
|
||
domain = lib.mkOption {
|
||
type = lib.types.str;
|
||
# Under the SWARM domain, like the forge and matrix: a swarm has one
|
||
# SSO provider, and the session cookie has to reach the swarm's
|
||
# services.
|
||
#
|
||
# Total on a null swarm domain so the required-domain assertion in
|
||
# hive-network.nix is the thing that fires; see the comment there.
|
||
default = if swarmDomain == null then "auth.invalid" else "auth.${swarmDomain}";
|
||
defaultText = lib.literalExpression ''"auth.''${services.hyperhive.swarm.domain}"'';
|
||
example = "login.example.com";
|
||
description = ''
|
||
Public hostname for the SSO provider — the sub-domain shape the
|
||
forge and matrix already use, under the swarm's domain because a
|
||
swarm has **one** SSO provider. Must be the name browsers
|
||
actually visit: it is the `authelia_url` the session cookie is
|
||
validated against.
|
||
|
||
⚠️ Unlike the forge and matrix names, this one carries **no
|
||
migration pin**: nothing depends on the previous
|
||
`auth.''${services.hyperhive.domain}` yet, so it moves outright.
|
||
'';
|
||
};
|
||
|
||
url = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.str;
|
||
default = if cfg.enable then "https://${cfg.domain}" else null;
|
||
defaultText = lib.literalExpression ''if enable then "https://''${domain}" else null'';
|
||
example = "https://auth.example.com";
|
||
description = ''
|
||
Base URL clients are sent to for authentication — the half of
|
||
this module that exists on **every** hive, not just the one
|
||
running the container.
|
||
|
||
Defaults to this host's own instance **only when this module is
|
||
the thing running it**; in that case the URL is not a guess, it
|
||
is where this module just put the container. Otherwise `null`,
|
||
and a hive that federates with a swarm sets it explicitly to
|
||
wherever the swarm's authelia lives. Null means "no SSO
|
||
configured" and consumers say so rather than inventing an
|
||
address — an endpoint baked in as a fallback is one that
|
||
resolves cleanly and points at the wrong machine.
|
||
'';
|
||
};
|
||
|
||
usersFile = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "${stateDir}/users.yml";
|
||
defaultText = lib.literalExpression ''"/var/lib/authelia-swarm/users.yml"'';
|
||
description = ''
|
||
Path (inside the container) of authelia's file users database.
|
||
|
||
Written by swarm-controller, not by hand: agents come and go
|
||
continuously, so the subject set is dynamic and belongs to a
|
||
program. This module only guarantees the file *exists* and is
|
||
valid YAML at first boot, so authelia starts with no subjects
|
||
rather than failing to start — a provider with nobody in it yet
|
||
is the correct state before anything has provisioned users.
|
||
'';
|
||
};
|
||
|
||
oidc.clients = lib.mkOption {
|
||
type = lib.types.listOf (
|
||
lib.types.submodule {
|
||
options = {
|
||
id = lib.mkOption {
|
||
type = lib.types.str;
|
||
example = "forgejo";
|
||
description = ''
|
||
OAuth2 client id, as the relying party knows itself.
|
||
'';
|
||
};
|
||
|
||
description = lib.mkOption {
|
||
type = lib.types.str;
|
||
example = "HyperHive forge";
|
||
description = ''
|
||
Human-readable name, shown on authelia's consent screen.
|
||
This is the string a person reads when deciding whether
|
||
to hand an application their identity, so it should name
|
||
the application rather than the protocol.
|
||
'';
|
||
};
|
||
|
||
redirectUris = lib.mkOption {
|
||
type = lib.types.listOf lib.types.str;
|
||
example = [ "https://forge.example.com/user/oauth2/authelia/callback" ];
|
||
description = ''
|
||
Exact callback URLs the provider will redirect to.
|
||
Matched literally by authelia — a trailing-slash
|
||
difference is a rejected login, not a warning.
|
||
'';
|
||
};
|
||
};
|
||
}
|
||
);
|
||
default = [ ];
|
||
description = ''
|
||
OIDC relying parties this provider will issue tokens to.
|
||
Declaring one turns the provider on; the default empty list
|
||
leaves this module exactly as it was — a session provider and
|
||
nothing else.
|
||
|
||
⚠️ **There is deliberately no secret here.** A client secret has
|
||
two holders in two containers (authelia keeps a *hash*, the
|
||
relying party the *plaintext*), and
|
||
`services.authelia.instances.<n>.settings` is rendered into the
|
||
**nix store**, which is world-readable and permanent. So this
|
||
option carries only the parts that are safe to evaluate: the
|
||
secret is minted on first boot and never passes through a nix
|
||
expression. See `docs/swarm/` for what goes where.
|
||
'';
|
||
};
|
||
|
||
# Derived facts, exposed for consumers that have to act on this
|
||
# container **from outside it** — `swarmctl` is the first, and it
|
||
# needs all three. Read-only options rather than literals repeated at
|
||
# the call site: the machine and unit names are derived from
|
||
# `instance` here, so a second copy elsewhere is a second thing to
|
||
# keep in step, and the one that drifts is the one nobody tests.
|
||
machine = lib.mkOption {
|
||
type = lib.types.str;
|
||
readOnly = true;
|
||
default = "swarm-authelia";
|
||
description = ''
|
||
Name of the nixos-container authelia runs in. Read-only: it is
|
||
what this module declares, published so callers of
|
||
`systemctl -M` and `/var/lib/nixos-containers/<name>` do not
|
||
have to hardcode it.
|
||
'';
|
||
};
|
||
|
||
unit = lib.mkOption {
|
||
type = lib.types.str;
|
||
readOnly = true;
|
||
default = "${unitName}.service";
|
||
description = ''
|
||
authelia's systemd unit *inside* the container. Read-only, and
|
||
derived from the instance name exactly like the unit itself.
|
||
'';
|
||
};
|
||
|
||
hostUsersFile = lib.mkOption {
|
||
type = lib.types.str;
|
||
readOnly = true;
|
||
default = "/var/lib/nixos-containers/${cfg.machine}${cfg.usersFile}";
|
||
description = ''
|
||
`usersFile` as seen from the **host** — the container's root
|
||
prefixed onto the path authelia sees.
|
||
|
||
The distinction is load-bearing: the users database is written
|
||
from the host by a program that does not live in this container,
|
||
while authelia only ever sees the inner path. Handing the wrong
|
||
one to either side yields a file nobody reads rather than an
|
||
error.
|
||
'';
|
||
};
|
||
};
|
||
|
||
config = lib.mkIf (hyperhiveCfg.enable && cfg.enable) {
|
||
containers.${cfg.machine} = {
|
||
autoStart = true;
|
||
ephemeral = false;
|
||
# Shared host netns, like the forge and matrix containers: the
|
||
# gateway reaches authelia at 127.0.0.1:<port>.
|
||
privateNetwork = false;
|
||
|
||
config =
|
||
{ ... }:
|
||
{
|
||
system.stateVersion = "26.05";
|
||
|
||
# The authelia binary itself, so an operator who gets a shell
|
||
# in here can run `authelia crypto hash generate` to make a
|
||
# password for the users file. Without it the container runs
|
||
# authelia and cannot invoke it: the unit's ExecStart resolves
|
||
# through the store path, and nothing puts the CLI on PATH.
|
||
environment.systemPackages = [ cfg.package ];
|
||
|
||
# This container shares the host netns, so its own
|
||
# firewall.service would rewrite the HOST ruleset at every
|
||
# boot. The host firewall owns all filtering.
|
||
networking.firewall.enable = false;
|
||
# Keep the host-copied /etc/resolv.conf intact — resolvconf's
|
||
# host-tracking would regenerate it to an empty file, since
|
||
# the host's copy doesn't cross the boundary after start.
|
||
networking.resolvconf.enable = lib.mkForce false;
|
||
|
||
# authelia's own secrets, generated in-container on first
|
||
# boot. They are jwt/session/storage keys — nothing outside
|
||
# this container ever reads them, which is what makes
|
||
# in-container generation right rather than merely easier.
|
||
# (hive-matrix generates its token host-side only because
|
||
# hive-c0re has to read that one.)
|
||
#
|
||
# Same `User`/`Group`/`StateDirectory` as the authelia unit,
|
||
# so systemd creates the directory owned by the account that
|
||
# has to read the files and this unit can write nowhere else.
|
||
# No chown, no mode juggling: authelia opens these paths
|
||
# itself, as its own user, under `PrivateUsers=true`.
|
||
systemd.services."${unitName}-secrets" = {
|
||
description = "Generate authelia's secrets on first boot";
|
||
wantedBy = [ "multi-user.target" ];
|
||
before = [ "${unitName}.service" ];
|
||
requiredBy = [ "${unitName}.service" ];
|
||
# `cfg.package` is here for its CLI, not its daemon: the
|
||
# client secrets are minted with `authelia crypto hash
|
||
# generate`, which is the only way to produce a digest in
|
||
# the exact form authelia will later verify.
|
||
path = [
|
||
pkgs.coreutils
|
||
]
|
||
++ lib.optionals oidcEnabled [
|
||
pkgs.openssl
|
||
cfg.package
|
||
];
|
||
serviceConfig = {
|
||
Type = "oneshot";
|
||
RemainAfterExit = true;
|
||
User = unitName;
|
||
Group = unitName;
|
||
StateDirectory = unitName;
|
||
StateDirectoryMode = "0700";
|
||
UMask = "0077";
|
||
SyslogIdentifier = "${unitName}-secrets";
|
||
};
|
||
script = ''
|
||
set -euo pipefail
|
||
|
||
# Each is generated once and never rotated here: the
|
||
# session and storage keys are load-bearing for data
|
||
# already written (sessions, the encrypted store), so
|
||
# replacing one is an operator action, not a boot action.
|
||
for f in ${lib.concatStringsSep " " randomKeys}; do
|
||
p=${lib.escapeShellArg stateDir}/"$f".key
|
||
if [ ! -s "$p" ]; then
|
||
head -c 64 /dev/urandom | od -An -tx1 | tr -d ' \n' > "$p"
|
||
echo "generated $p"
|
||
fi
|
||
chmod 0600 "$p"
|
||
done
|
||
${oidcGenScript}
|
||
|
||
# A users database that exists and parses, with nobody in
|
||
# it. authelia refuses to start without one, and the
|
||
# alternative to an empty file is a placeholder account —
|
||
# which is a credential nobody meant to create.
|
||
users=${lib.escapeShellArg cfg.usersFile}
|
||
if [ ! -s "$users" ]; then
|
||
echo "users: {}" > "$users"
|
||
echo "seeded empty users database at $users"
|
||
fi
|
||
chmod 0600 "$users"
|
||
'';
|
||
};
|
||
|
||
services.authelia.instances.${instance} = {
|
||
enable = true;
|
||
package = cfg.package;
|
||
|
||
# Merged at RUNTIME alongside the nix-generated config, which
|
||
# is the whole reason the client digests can exist at all:
|
||
# `settings` below is rendered into the world-readable nix
|
||
# store, and a client secret's digest may not go there.
|
||
# Empty (and inert) on a hive with no clients declared.
|
||
settingsFiles = lib.optional oidcEnabled clientsFile;
|
||
|
||
secrets = {
|
||
jwtSecretFile = "${stateDir}/jwt.key";
|
||
sessionSecretFile = "${stateDir}/session.key";
|
||
storageEncryptionKeyFile = "${stateDir}/storage-encryption.key";
|
||
}
|
||
// lib.optionalAttrs oidcEnabled {
|
||
# Both are `LoadCredential`-delivered by upstream's module,
|
||
# so they reach authelia as `AUTHELIA_*_FILE` env and never
|
||
# as values. Same by-path discipline as the three above —
|
||
# which is what lets the provider's own secrets stay
|
||
# in-container: nothing outside this container reads them.
|
||
#
|
||
# ⚠️ The issuer key is NOT passed as the deprecated
|
||
# `issuer_private_key`: on 4.38+ the config key is the
|
||
# `jwks` *list*, and a list element cannot take the
|
||
# `_FILE` env treatment. Upstream's module bridges that
|
||
# by generating a small settings file that templates the
|
||
# PEM in (`{{ secret "<path>" }}`) and prepending it to
|
||
# `settingsFiles`. So handing it a path here is the
|
||
# modern shape, not the legacy one — checked against the
|
||
# pin (nixpkgs `services/security/authelia.nix`), because
|
||
# the option name alone reads like the old key.
|
||
oidcHmacSecretFile = "${stateDir}/oidc-hmac.key";
|
||
oidcIssuerPrivateKeyFile = "${stateDir}/oidc-issuer.key";
|
||
};
|
||
|
||
# Small-deployment defaults, and the scope is the
|
||
# justification: one swarm, one authelia, no replicas.
|
||
# - file users backend, written by swarm-controller
|
||
# - local sqlite storage: redis buys shared session state
|
||
# across replicas, and there is one instance
|
||
# - filesystem notifier: SMTP is for mailing humans, and
|
||
# provisioning is programmatic; a file is honest about
|
||
# where those messages go instead of implying a mail path
|
||
settings = {
|
||
theme = "dark";
|
||
server.address = "tcp://127.0.0.1:${toString cfg.port}";
|
||
log.level = "info";
|
||
|
||
authentication_backend.file.path = cfg.usersFile;
|
||
|
||
access_control.default_policy = "one_factor";
|
||
|
||
# The cookie domain is the SWARM's domain, NOT authelia's
|
||
# own host: the session cookie has to be sent to the apps
|
||
# being protected (`forge.<swarm>`, `chat.<swarm>`), and a
|
||
# cookie scoped to `auth.<swarm>` reaches none of them.
|
||
# authelia enforces the relationship from the other side
|
||
# too — `authelia_url` must be a sub-domain of `domain`, so
|
||
# setting both to the same host fails validation at startup
|
||
# rather than at first login.
|
||
#
|
||
# ⚠️ Known and accepted consequence while a hive keeps a
|
||
# domain outside the swarm's tree: this cookie is NOT sent
|
||
# to that hive's own surfaces (its dashboard), so SSO
|
||
# covers the swarm's services and not the hive's. It
|
||
# resolves when the hive domain moves under the swarm
|
||
# domain; until then it is a scope limit, not a bug to
|
||
# chase.
|
||
session.cookies = [
|
||
{
|
||
domain = cookieDomain;
|
||
authelia_url = "https://${cfg.domain}";
|
||
}
|
||
];
|
||
|
||
storage.local.path = "${stateDir}/db.sqlite3";
|
||
notifier.filesystem.filename = "${stateDir}/notification.txt";
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
}
|