feat(swarm-authelia): one machine client per hive in the roster
Second slice of #3274. Each entry in `services.hyperhive.swarm.hives` gets an authelia client `hive-<name>`, so a hive can authenticate to swarm services as itself. The clients are derived here rather than declared by a consuming service, because a hive's identity belongs to the directory and not to whichever service happens to use it first. mara's constraint is that a hive's credentials "derive from the same identity" — one hive holds one client and mints a different token per service from it — so the queue declaring this list would mean the next consumer collides on the same client id, and only at the moment it landed. `oidc.hiveIdentities` defaults to whether the message queue is enabled, that being the first service to need a hive to prove who it is. It is an option rather than a hard-coded condition so the swarm telemetry collector can turn it on without the queue. No audience and no `access_token_signed_response_alg` here: the queue learns which hive presented a token from the introspection response (`{"active":true,"client_id":"hive-alpha",...}`, measured), so it needs neither. The collector does need both, and they land with it. Also asserts client ids are unique — newly reachable now that part of the list is derived, since a hive named `x` and a service client named `hive-x` never met before. Refs #3274.
This commit is contained in:
parent
8542f2ca42
commit
b3f46e4f43
1 changed files with 78 additions and 11 deletions
|
|
@ -72,12 +72,35 @@ let
|
|||
# be what an operator sees, not a coercion error from here.
|
||||
cookieDomain = if swarmDomain == null then "invalid" else swarmDomain;
|
||||
|
||||
# One machine client per hive in the roster. A hive's identity belongs
|
||||
# to the DIRECTORY, not to whichever service happens to consume it:
|
||||
# the rule is that a hive's credentials all derive from the SAME
|
||||
# identity, so one hive holds ONE client and mints a different token
|
||||
# per service from it. Were the queue to declare this list, the next
|
||||
# consumer would collide on the same client id — and only at the
|
||||
# moment it landed.
|
||||
hiveClients = lib.mapAttrsToList (name: _: {
|
||||
id = "hive-${name}";
|
||||
description = "HyperHive hive ${name}";
|
||||
kind = "machine";
|
||||
redirectUris = [ ];
|
||||
}) hyperhiveCfg.swarm.hives;
|
||||
|
||||
# Everything downstream renders and mints from this, not from the
|
||||
# declared list alone.
|
||||
allClients = cfg.oidc.clients ++ lib.optionals cfg.oidc.hiveIdentities hiveClients;
|
||||
|
||||
# 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 != [ ];
|
||||
#
|
||||
# ⚠️ Derived from `allClients`, so hive identities can turn the provider
|
||||
# on by themselves. That is only reachable where the queue is already
|
||||
# enabled (`hiveIdentities` defaults to it) — and a queue-enabled hive
|
||||
# already contributes a client, so no existing deployment flips.
|
||||
oidcEnabled = allClients != [ ];
|
||||
|
||||
# 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).
|
||||
|
|
@ -193,7 +216,7 @@ let
|
|||
|
||||
${lib.concatMapStrings (c: ''
|
||||
mint ${lib.escapeShellArg c.id}
|
||||
'') cfg.oidc.clients}
|
||||
'') allClients}
|
||||
|
||||
# Re-rendered every boot, deliberately: the secret is minted once,
|
||||
# but the metadata around it (a new redirect URI, a renamed client)
|
||||
|
|
@ -204,7 +227,7 @@ let
|
|||
printf -- 'identity_providers:\n'
|
||||
printf -- ' oidc:\n'
|
||||
printf -- ' clients:\n'
|
||||
${lib.concatMapStrings renderClient cfg.oidc.clients}
|
||||
${lib.concatMapStrings renderClient allClients}
|
||||
} > ${lib.escapeShellArg "${clientsFile}.tmp"}
|
||||
chmod 0600 ${lib.escapeShellArg "${clientsFile}.tmp"}
|
||||
mv ${lib.escapeShellArg "${clientsFile}.tmp"} ${lib.escapeShellArg clientsFile}
|
||||
|
|
@ -308,6 +331,29 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
oidc.hiveIdentities = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = hyperhiveCfg.swarm.nats.enable;
|
||||
defaultText = lib.literalExpression "services.hyperhive.swarm.nats.enable";
|
||||
description = ''
|
||||
Mint one machine client per hive in
|
||||
{option}`services.hyperhive.swarm.hives`, so each hive can
|
||||
authenticate to swarm services as itself.
|
||||
|
||||
Defaults to whether the swarm message queue is enabled, because
|
||||
that is the first service that needs a hive to prove who it is.
|
||||
It is an option rather than a hard-coded condition so a second
|
||||
consumer — the swarm telemetry collector — can turn it on
|
||||
without the queue, and so a swarm that wants the identities
|
||||
provisioned ahead of either can say so.
|
||||
|
||||
The clients are inert until something authenticates with them:
|
||||
each is a client id and a secret sitting on this host. What
|
||||
delivers a secret to a hive that is not this host is a separate
|
||||
problem and deliberately not solved here.
|
||||
'';
|
||||
};
|
||||
|
||||
oidc.clients = lib.mkOption {
|
||||
type = lib.types.listOf (
|
||||
lib.types.submodule {
|
||||
|
|
@ -464,14 +510,35 @@ in
|
|||
# means whoever wrote it believes a browser is involved. Failing here
|
||||
# is how that belief gets corrected at the point it was expressed,
|
||||
# rather than at a token endpoint months later.
|
||||
assertions = map (c: {
|
||||
assertion = c.kind != "machine" || c.redirectUris == [ ];
|
||||
message =
|
||||
"services.hyperhive.swarm.authelia.oidc.clients: client '${c.id}' is "
|
||||
+ "kind = \"machine\" but declares redirectUris. A client_credentials "
|
||||
+ "client has nobody to redirect; drop the URIs or make it "
|
||||
+ "kind = \"interactive\".";
|
||||
}) cfg.oidc.clients;
|
||||
assertions =
|
||||
map (c: {
|
||||
assertion = c.kind != "machine" || c.redirectUris == [ ];
|
||||
message =
|
||||
"services.hyperhive.swarm.authelia.oidc.clients: client '${c.id}' is "
|
||||
+ "kind = \"machine\" but declares redirectUris. A client_credentials "
|
||||
+ "client has nobody to redirect; drop the URIs or make it "
|
||||
+ "kind = \"interactive\".";
|
||||
}) allClients
|
||||
# Two clients sharing an id renders two YAML entries under one name.
|
||||
# Newly reachable now that part of the list is DERIVED: a hive called
|
||||
# `x` and a service client called `hive-x` never met before. Authelia
|
||||
# would reject it, but three layers away and at boot — naming both
|
||||
# sources here is the cheaper failure.
|
||||
++ [
|
||||
{
|
||||
assertion = lib.length (lib.unique (map (c: c.id) allClients)) == lib.length allClients;
|
||||
message =
|
||||
"services.hyperhive.swarm.authelia: duplicate OIDC client id(s): "
|
||||
+ lib.concatStringsSep ", " (
|
||||
lib.unique (
|
||||
lib.filter (id: lib.count (x: x == id) (map (c: c.id) allClients) > 1) (map (c: c.id) allClients)
|
||||
)
|
||||
)
|
||||
+ ". Hive identities are named `hive-<name>` from "
|
||||
+ "services.hyperhive.swarm.hives; rename the hive or the "
|
||||
+ "colliding client.";
|
||||
}
|
||||
];
|
||||
|
||||
# Authelia's own gateway surface: the vhost that fronts it and the
|
||||
# name the hive resolver answers for. Both live here rather than in
|
||||
|
|
|
|||
Loading…
Reference in a new issue