keep authelia's keys in bao, seeded from the existing key files
This commit is contained in:
parent
a70b25847e
commit
99fab06348
1 changed files with 557 additions and 89 deletions
|
|
@ -180,6 +180,8 @@ let
|
|||
instance = lib.removePrefix "authelia-" unitName;
|
||||
stateDir = "/var/lib/${unitName}";
|
||||
atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
|
||||
refreshConsumer = import ./lib/refresh-consumer.nix { };
|
||||
storeSeededSecret = import ./lib/store-seeded-secret.nix { };
|
||||
|
||||
caTrust = import ./lib/hive-ca-trust.nix {
|
||||
inherit lib gatewayCfg;
|
||||
|
|
@ -464,21 +466,22 @@ let
|
|||
# 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"
|
||||
|
||||
${lib.optionalString (!publishes) ''
|
||||
# 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,
|
||||
|
|
@ -534,6 +537,68 @@ let
|
|||
mv ${lib.escapeShellArg "${clientsFile}.tmp"} ${lib.escapeShellArg clientsFile}
|
||||
'';
|
||||
|
||||
storeRetry = import ./lib/store-retry.nix { };
|
||||
|
||||
# authelia's keys, one leaf each under its container's name. The nix half of
|
||||
# `autheliaKeyLeaves` in ./swarm-bao.nix, which grants the publisher
|
||||
# `create` and `read` on exactly these.
|
||||
keyLeaf = name: "secret/swarm/services/${cfg.machine}/keys/${name}";
|
||||
|
||||
# The keys as a file-per-key script, for a container with no store identity:
|
||||
# each is generated once and never rotated here. The session and storage
|
||||
# keys are load-bearing for data already written (sessions, the encrypted
|
||||
# database), so replacing one is an operator action, not a boot action.
|
||||
fileKeysScript = ''
|
||||
for f in ${lib.concatStringsSep " " randomKeys}; do
|
||||
p=${lib.escapeShellArg stateDir}/"$f".key
|
||||
if [ ! -s "$p" ]; then
|
||||
value=$(head -c 64 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
||||
atomic_write_secret 0600 "" "$p" "$value"
|
||||
echo "generated $p"
|
||||
fi
|
||||
chmod 0600 "$p"
|
||||
done
|
||||
'';
|
||||
|
||||
# The same keys from the store, at the same paths. The store's value wins,
|
||||
# then the file's, which seeds the store when it holds none, so a key never
|
||||
# changes value by moving; a new one is minted only where neither exists.
|
||||
# The issuer key is an RSA pair rather than a random blob because it signs
|
||||
# id tokens, which relying parties verify against the public half.
|
||||
storeKeysScript = ''
|
||||
${storeSeededSecret}
|
||||
${refreshConsumer}
|
||||
store_init
|
||||
store_login
|
||||
|
||||
mint_hex() {
|
||||
head -c 64 /dev/urandom | od -An -tx1 | tr -d ' \n'
|
||||
}
|
||||
mint_issuer() {
|
||||
openssl genrsa 4096
|
||||
}
|
||||
|
||||
# <name> <mint command…>. A changed file reaches a running authelia by
|
||||
# restarting it; it reads its keys once, as it starts.
|
||||
keep_key() {
|
||||
local name="$1" p value
|
||||
p=${lib.escapeShellArg stateDir}/"$name".key
|
||||
shift
|
||||
value="$(store_seeded ${lib.escapeShellArg (keyLeaf "")}"$name" "$p" "$@")" || exit 1
|
||||
if secret_differs "$p" "$value"; then
|
||||
atomic_write_secret 0600 "" "$p" "$value"
|
||||
echo "wrote $p"
|
||||
refresh_unit ${lib.escapeShellArg cfg.unit} "$p"
|
||||
fi
|
||||
chmod 0600 "$p"
|
||||
}
|
||||
|
||||
for f in ${lib.concatStringsSep " " randomKeys}; do
|
||||
keep_key "$f" mint_hex
|
||||
done
|
||||
${lib.optionalString oidcEnabled "keep_key oidc-issuer mint_issuer"}
|
||||
'';
|
||||
|
||||
# A netns of its own on the hive bridge: the container dials the store, the
|
||||
# swarm collector and its own public name through the bridge, and the
|
||||
# gateway reaches the three listeners below over sockets, so authelia binds
|
||||
|
|
@ -955,7 +1020,7 @@ in
|
|||
(import ../container-modules/swarm-container-resolver.nix {
|
||||
inherit (networkCfg) bridgeIp;
|
||||
# The forwarder dials the swarm collector and authelia by name,
|
||||
# and its fetch and the publisher the store.
|
||||
# and its fetch, the publisher and the key unit the store.
|
||||
dnsConsumers = [
|
||||
cfg.unit
|
||||
]
|
||||
|
|
@ -963,6 +1028,7 @@ in
|
|||
"opentelemetry-collector.service"
|
||||
"${forwarderFetchUnit}.service"
|
||||
"${publisherUnit}.service"
|
||||
"${unitName}-secrets.service"
|
||||
];
|
||||
})
|
||||
caBundleModule
|
||||
|
|
@ -1022,12 +1088,12 @@ in
|
|||
# through the store path, and nothing puts the CLI on PATH.
|
||||
environment.systemPackages = [ deployCfg.authelia.package ];
|
||||
|
||||
# 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.)
|
||||
# authelia's own keys: jwt, session, storage encryption, OIDC HMAC
|
||||
# and the OIDC issuer. Nothing outside this container reads them.
|
||||
# With the publisher's store identity the swarm secret store holds
|
||||
# them and this unit copies them into place, seeding the store from
|
||||
# the files an earlier release generated here when it holds none;
|
||||
# without it the unit generates them on first boot, as before.
|
||||
#
|
||||
# Same `User`/`Group`/`StateDirectory` as the authelia unit,
|
||||
# so systemd creates the directory owned by the account that
|
||||
|
|
@ -1049,7 +1115,15 @@ in
|
|||
++ lib.optionals oidcEnabled [
|
||||
pkgs.openssl
|
||||
deployCfg.authelia.package
|
||||
]
|
||||
++ lib.optionals publishes [
|
||||
deployCfg.bao.package
|
||||
pkgs.systemd
|
||||
];
|
||||
# A store that cannot answer, with a key missing on disk, is a
|
||||
# failed run that retries (./lib/store-retry.nix).
|
||||
startLimitBurst = lib.mkIf publishes storeRetry.startLimitBurst;
|
||||
startLimitIntervalSec = lib.mkIf publishes storeRetry.startLimitIntervalSec;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
|
|
@ -1059,25 +1133,31 @@ in
|
|||
StateDirectoryMode = "0700";
|
||||
UMask = "0077";
|
||||
SyslogIdentifier = "${unitName}-secrets";
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs publishes (
|
||||
storeRetry.serviceConfig
|
||||
// {
|
||||
LoadCredential = lib.attrNames storeFiles;
|
||||
TimeoutStartSec = 120;
|
||||
}
|
||||
);
|
||||
environment = lib.optionalAttrs publishes (
|
||||
let
|
||||
store = storeIdentityIn "${unitName}-secrets";
|
||||
in
|
||||
{
|
||||
BAO_ADDR = store.addr;
|
||||
BAO_CLIENT_CERT = store.clientCertFile;
|
||||
BAO_CLIENT_KEY = store.clientKeyFile;
|
||||
}
|
||||
// lib.optionalAttrs (store.caFile != null) { BAO_CACERT = store.caFile; }
|
||||
);
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
|
||||
${atomicWriteSecret}
|
||||
|
||||
# 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
|
||||
value=$(head -c 64 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
||||
atomic_write_secret 0600 "" "$p" "$value"
|
||||
echo "generated $p"
|
||||
fi
|
||||
chmod 0600 "$p"
|
||||
done
|
||||
${if publishes then storeKeysScript else fileKeysScript}
|
||||
${oidcGenScript}
|
||||
|
||||
# authelia exits at startup on an empty user set, so an empty
|
||||
|
|
|
|||
Loading…
Reference in a new issue