Watch
0
0
Fork
You've already forked hyperhive
0

swarmctl: log in with the operator's own SSO account and reach the controller through the swarm domain

`swarmctl --swarm https://<swarm> login` runs the OAuth 2.0
authorization-code flow with PKCE against the swarm's authelia as a new
public `swarmctl` client: a pushed authorization request, the browser login
and explicit consent, and the code delivered by form_post to a loopback
port. The access and refresh tokens are cached at mode 0600 under
$XDG_STATE_HOME/swarmctl/ and refreshed under a file lock, since authelia
rotates the refresh token and revokes the login when a used one is
presented again. With --swarm (or SWARMCTL_SWARM), the agent, schedule and
forge verbs send their requests to https://<swarm>/api/ with the bearer
token instead of over the controller's unix socket, which stays the default.

authelia registers the client on every authelia host with a swarm domain:
`kind = "native"`, public, no minted secret, scopes offline_access and
authelia.bearer.authz, audience https://<swarm>/api/ (matched as a URL
prefix), redirect http://127.0.0.1/callback (any loopback port). Every
field is one authelia 4.39.20's validator requires for a public client
holding the bearer scope.

The swarm apex's /api/ answers a refused caller that sent an
Authorization header with the 401 itself instead of the login redirect,
so the CLI can tell "log in again" apart from an answer; a browser still
gets the redirect. The bearer token is not forwarded to the controller.

Refs #4409 (mara: c94764, c94782).
This commit is contained in:
atlas 2026-10-11 23:43:29 +02:00 • committed by mara
commit 2df19e4899
17 changed files with 1118 additions and 122 deletions

View file

@ -234,6 +234,52 @@ let
in
v.forceSSL && !(v.addSSL or false);
}
{
# `swarmctl --swarm` reads a 401 as "log in again"; a 302 would hand it
# the login page instead. The `/` clause is the control: the browser
# route still redirects, so a pass means the two routes differ.
name = "the controller API answers a refused bearer caller 401 and a browser the login";
ok =
let
l = allLocal.services.nginx.virtualHosts."t.local".locations;
api = l."/api/".extraConfig;
refused = l."@swarm_api_unauthorized".extraConfig;
in
lib.hasInfix "error_page 401 = @swarm_api_unauthorized;" api
&& !(lib.hasInfix "=302" api)
&& lib.hasInfix ''proxy_set_header Authorization "";'' api
&& lib.hasInfix "if ($http_authorization) {\n return 401;\n}" refused
&& lib.hasInfix "return 302 https://auth.t.local/?rd=$target_url;" refused
&& lib.hasInfix "error_page 401 =302 https://auth.t.local/" l."/".extraConfig;
}
{
# authelia 4.39.20 refuses a bearer-scoped public client missing any of
# these at startup, after a green deploy; and a public client must not
# be handed a secret.
name = "the swarmctl login client is public, bearer-scoped to the controller API, and gets no secret";
ok =
let
s = allLocal.containers.swarm-authelia.config.systemd.services.authelia-swarm-secrets.script;
entry = lib.last (lib.splitString "client_id: %s\\n' swarmctl" s);
client = lib.head (lib.splitString "client_id: %s" entry);
in
lib.all (line: lib.hasInfix line client) [
"public: true"
"token_endpoint_auth_method: none"
"require_pkce: true"
"pkce_challenge_method: S256"
"require_pushed_authorization_requests: true"
"consent_mode: explicit"
"response_modes: [form_post]"
"grant_types: [authorization_code, refresh_token]"
"scopes: [offline_access, authelia.bearer.authz]"
"audience: [%s]\\n' https://t.local/api/"
"' - %s\\n' http://127.0.0.1/callback"
]
&& !(lib.hasInfix "client_secret" client)
&& !(lib.hasInfix "mint swarmctl" s)
&& lib.hasInfix "mint swarm-authelia-bridge" s;
}
{
name = "a hive with matrix off serves no matrix discovery endpoint";
ok =