wip: nix unit + secret delivery for the callout responder
This commit is contained in:
parent
3a75c54bcb
commit
207fc4d2a6
3 changed files with 142 additions and 0 deletions
|
|
@ -151,6 +151,9 @@
|
||||||
services.hyperhive.swarm.ui.package =
|
services.hyperhive.swarm.ui.package =
|
||||||
lib.mkDefault
|
lib.mkDefault
|
||||||
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-ui;
|
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-ui;
|
||||||
|
services.hyperhive.swarm.nats.authPackage =
|
||||||
|
lib.mkDefault
|
||||||
|
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-nats-auth;
|
||||||
services.hyperhive.gateway.swaggerUiTheme =
|
services.hyperhive.gateway.swaggerUiTheme =
|
||||||
lib.mkDefault
|
lib.mkDefault
|
||||||
self.packages.${pkgs.stdenv.hostPlatform.system}.swagger-ui-theme;
|
self.packages.${pkgs.stdenv.hostPlatform.system}.swagger-ui-theme;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,22 @@ let
|
||||||
# authorizes.
|
# authorizes.
|
||||||
calloutAccount = "AUTH";
|
calloutAccount = "AUTH";
|
||||||
clientAccount = "APP";
|
clientAccount = "APP";
|
||||||
|
|
||||||
|
machine = "swarm-nats";
|
||||||
|
# Where the responder's credentials live *inside* the container, and the
|
||||||
|
# host path that resolves to. Two names for one location, because the
|
||||||
|
# host is the only place both filesystems are addressable.
|
||||||
|
secretDirInContainer = "/var/lib/swarm-nats-auth";
|
||||||
|
inContainer = name: "${secretDirInContainer}/${name}";
|
||||||
|
secretDir = "/var/lib/nixos-containers/${machine}${secretDirInContainer}";
|
||||||
|
hostPath = name: "${secretDir}/${name}";
|
||||||
|
|
||||||
|
# The responder needs all three credentials. Gating on them rather than
|
||||||
|
# on `cfg.enable` keeps a half-configured hive at "queue up, denying
|
||||||
|
# everyone" instead of "unit crash-looping on a missing file".
|
||||||
|
responderConfigured = cfg.calloutUserSeedFile != "" && cfg.calloutIssuerSeedFile != "";
|
||||||
|
clientSecretSource = "${autheliaCfg.hostClientSecretDir}/${cfg.clientId}.secret";
|
||||||
|
introspectionUrl = "${toString autheliaUrl}/api/oidc/introspection";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# The swarm's message queue: one NATS server, reached by every hive.
|
# The swarm's message queue: one NATS server, reached by every hive.
|
||||||
|
|
@ -116,6 +132,55 @@ in
|
||||||
rather than on purpose, so it fails at eval instead.
|
rather than on purpose, so it fails at eval instead.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
authPackage = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-nats-auth";
|
||||||
|
description = ''
|
||||||
|
The auth-callout responder package.
|
||||||
|
|
||||||
|
⚠️ Named `authPackage`, not `package`, on purpose: this module
|
||||||
|
deliberately has **no** `package` option for the server itself
|
||||||
|
(see the note above — upstream's `services.nats` resolves
|
||||||
|
`pkgs.nats-server` on its own), so a bare `package` here would
|
||||||
|
read as "the NATS package" and mean something else entirely.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
calloutUserSeedFile = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "";
|
||||||
|
example = "/run/secrets/swarm-nats-callout-user.seed";
|
||||||
|
description = ''
|
||||||
|
Absolute host path to the **seed** whose public half is
|
||||||
|
`calloutUserPublicKey`. The responder authenticates to the queue
|
||||||
|
with it.
|
||||||
|
|
||||||
|
A `str` rather than a `path`, and the reason is not style: a
|
||||||
|
`path`-typed literal is hash-copied into the world-readable nix
|
||||||
|
store at eval time, which is the opposite of what a seed wants.
|
||||||
|
Same discipline as `otel.headersCredential`.
|
||||||
|
|
||||||
|
Until this is set the responder cannot start, and the queue
|
||||||
|
stays in its fail-closed state — which is the correct behaviour,
|
||||||
|
not a gap.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
calloutIssuerSeedFile = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "";
|
||||||
|
example = "/run/secrets/swarm-nats-issuer.seed";
|
||||||
|
description = ''
|
||||||
|
Absolute host path to the **account** seed whose public half is
|
||||||
|
`calloutIssuerPublicKey`. The responder signs the user JWTs it
|
||||||
|
issues with it, so possession of this file is the authority to
|
||||||
|
admit anyone to the queue.
|
||||||
|
|
||||||
|
A `str` for the same store-leak reason as
|
||||||
|
`calloutUserSeedFile`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
@ -276,6 +341,46 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# The auth-callout responder: the half that lets the server
|
||||||
|
# above say *yes*. Without it the `auth_callout` block is a
|
||||||
|
# door nobody can open, which is the deliberate interim state.
|
||||||
|
#
|
||||||
|
# ⚠️ It is gated on the seeds being configured rather than on
|
||||||
|
# `cfg.enable`, so a half-configured hive gets a running,
|
||||||
|
# refusing queue instead of a unit that crash-loops on a
|
||||||
|
# missing file. A queue that denies everyone is a legible
|
||||||
|
# failure; a restart loop is not.
|
||||||
|
systemd.services.swarm-nats-auth = lib.mkIf responderConfigured {
|
||||||
|
description = "swarm queue auth-callout responder";
|
||||||
|
after = [ "nats.service" ];
|
||||||
|
requires = [ "nats.service" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = lib.concatStringsSep " " [
|
||||||
|
"${cfg.authPackage}/bin/swarm-nats-auth"
|
||||||
|
"--nats-url nats://127.0.0.1:${toString cfg.port}"
|
||||||
|
"--user-seed-file \${CREDENTIALS_DIRECTORY}/callout-user.seed"
|
||||||
|
"--issuer-seed-file \${CREDENTIALS_DIRECTORY}/issuer.seed"
|
||||||
|
"--client-secret-file \${CREDENTIALS_DIRECTORY}/oidc-client.secret"
|
||||||
|
"--client-id ${lib.escapeShellArg cfg.clientId}"
|
||||||
|
"--introspection-url ${lib.escapeShellArg introspectionUrl}"
|
||||||
|
];
|
||||||
|
# Every credential arrives by `LoadCredential` and is named
|
||||||
|
# on the command line only as a **path** — `argv` is
|
||||||
|
# world-readable via /proc/<pid>/cmdline, so a value there
|
||||||
|
# would be readable by every process on the host netns.
|
||||||
|
LoadCredential = [
|
||||||
|
"callout-user.seed:${inContainer "callout-user.seed"}"
|
||||||
|
"issuer.seed:${inContainer "issuer.seed"}"
|
||||||
|
"oidc-client.secret:${inContainer "oidc-client.secret"}"
|
||||||
|
];
|
||||||
|
DynamicUser = true;
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "5s";
|
||||||
|
SyslogIdentifier = "swarm-nats-auth";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# The server binary, so an operator with a shell in here can
|
# The server binary, so an operator with a shell in here can
|
||||||
# run `nats-server -t` against the generated config. The unit
|
# run `nats-server -t` against the generated config. The unit
|
||||||
# resolves ExecStart through the store path and puts nothing
|
# resolves ExecStart through the store path and puts nothing
|
||||||
|
|
@ -283,5 +388,33 @@ in
|
||||||
environment.systemPackages = [ pkgs.nats-server ];
|
environment.systemPackages = [ pkgs.nats-server ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Deliver the responder's three credentials into the container before
|
||||||
|
# it starts. Same shape as `hive-matrix-oidc-secret`, and for the same
|
||||||
|
# reason it is a copy rather than a `bindMounts` entry: nixos-container
|
||||||
|
# refuses to start when a bind source is missing, so one absent seed
|
||||||
|
# would take down the **whole container including the queue**, not
|
||||||
|
# merely the responder. A far larger blast radius than the fault.
|
||||||
|
systemd.services.swarm-nats-auth-secrets = lib.mkIf responderConfigured {
|
||||||
|
description = "deliver the swarm queue responder's credentials";
|
||||||
|
before = [ "container@swarm-nats.service" ];
|
||||||
|
wantedBy = [ "container@swarm-nats.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
SyslogIdentifier = "swarm-nats-auth-secrets";
|
||||||
|
};
|
||||||
|
path = [ pkgs.coreutils ];
|
||||||
|
script = ''
|
||||||
|
set -euo pipefail
|
||||||
|
install -d -m 0700 ${lib.escapeShellArg secretDir}
|
||||||
|
install -m 0400 ${lib.escapeShellArg cfg.calloutUserSeedFile} \
|
||||||
|
${lib.escapeShellArg (hostPath "callout-user.seed")}
|
||||||
|
install -m 0400 ${lib.escapeShellArg cfg.calloutIssuerSeedFile} \
|
||||||
|
${lib.escapeShellArg (hostPath "issuer.seed")}
|
||||||
|
install -m 0400 ${lib.escapeShellArg clientSecretSource} \
|
||||||
|
${lib.escapeShellArg (hostPath "oidc-client.secret")}
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,12 @@ in
|
||||||
# Uses the same per-bin extractor, just bound on its own.
|
# Uses the same per-bin extractor, just bound on its own.
|
||||||
swarm-controller = mkBinPackage "swarm-controller" "hyperhive swarm-level controller daemon";
|
swarm-controller = mkBinPackage "swarm-controller" "hyperhive swarm-level controller daemon";
|
||||||
|
|
||||||
|
# The queue's auth-callout responder. Out of `daemonBins` for the same
|
||||||
|
# reason as the two above and one more: it runs *inside* the swarm-nats
|
||||||
|
# container, not on the host, so it belongs in that container's closure
|
||||||
|
# rather than every hive's.
|
||||||
|
swarm-nats-auth = mkBinPackage "swarm-nats-auth" "hyperhive swarm queue auth-callout responder";
|
||||||
|
|
||||||
# The swarm operator's CLI, out of `daemonBins` for the same reason as
|
# The swarm operator's CLI, out of `daemonBins` for the same reason as
|
||||||
# the daemon above: it is installed by the swarm-controller module on
|
# the daemon above: it is installed by the swarm-controller module on
|
||||||
# the one host that runs the controller, and belongs in that hive's
|
# the one host that runs the controller, and belongs in that hive's
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue