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:
parent
5c15580060
commit
2df19e4899
17 changed files with 1118 additions and 122 deletions
|
|
@ -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 =
|
||||
|
|
|
|||
Loading…
Reference in a new issue