Single sign-on with an OIDC provider#
You will end up with people signing in to the router through your identity provider, with their group membership deciding their role — so revoking a group at the IdP removes router write access at the next sign-in without anybody editing Wheelhouse.
The agent is an OIDC relying party: authorization code plus PKCE, ID-token verification, one provider, configured by flags on the systemd unit.
Before you start#
- Console or SSH access to the router. This is configured by flags, not through the UI — an agent whose IdP settings can be edited through the UI that the IdP gates has a bootstrapping problem.
- An OIDC provider you control, and the ability to register a client on it.
- The groups you will map to roles.
Step 1 — Register the client at the provider#
Register exactly one redirect URI:
https://<router>:8443/api/oidc/callbackNote the client id and client secret.
Step 2 — Put the secret in a file#
sudo sh -c 'umask 077; cat > /config/wheelhouse/oidc-secret'
# paste the secret, then ctrl-DThe agent refuses to read a secret file that is group- or world-readable. /config is
the persistent partition, so the file survives an image upgrade; /config/wheelhouse/ is
the convention this documentation keeps so the secret lives beside the others. Nothing reads
it until you add the flag.
Step 3 — Add the flags with a drop-in#
Not by editing the packaged unit, which an image upgrade replaces.
[Service]
ExecStart=
ExecStart=/usr/bin/wheelhouse-agent … \
--oidc-issuer https://auth.example.com/application/o/wheelhouse/ \
--oidc-client-id wheelhouse \
--oidc-client-secret-file /config/wheelhouse/oidc-secret \
--oidc-redirect-url https://192.0.2.1:8443/api/oidc/callback \
--oidc-admin-groups homelab-admins \
--oidc-operator-groups wheelhouse-opsRepeat every flag the packaged unit already passes; the empty ExecStart= clears the
original.
sudo systemctl daemon-reload
sudo systemctl restart wheelhouse-agentPin --oidc-redirect-url if the agent is reachable under more than one name, so the
flow never depends on which name the browser happened to use. Without it the URL is derived
per request from the incoming origin.
Step 4 — Understand what an assertion is allowed to do#
These four rules are what stop SSO becoming a back door, and they are worth knowing before you turn it on.
Link is by subject. sub is the IdP's immutable identifier; the first match wins and
the session is that user's. An empty password hash with a subject means the account is
SSO-only.
An unknown identity is provisioned, or refused. --oidc-provision (default true)
creates an account named from --oidc-username-claim (default preferred_username) with
--oidc-default-role (default viewer). If a local account already has that name and no
subject, the login is refused — provisioning never attaches an assertion to an account
that already exists.
Groups decide role when --oidc-admin-groups or --oidc-operator-groups are set. The
claim is --oidc-groups-claim (default groups; some providers nest it differently).
Mapping will not demote the only admin.
Email is never an identity by default. --oidc-link-by-email exists, is off, and
refuses an address the IdP has not marked email_verified.
Step 5 — Sign in#
The login screen grows an SSO button once the provider answers. GET /api/oidc/status is
deliberately honest: a configured provider that is not answering reports ready: false with
the reason, so the login screen says so rather than showing a button that does nothing.
Two consequences worth knowing#
Password login, TOTP, API tokens and the break-glass token all keep working. An agent that can only be entered through an IdP is one IdP outage from being an unmanageable router. That is deliberate.
The session cookie is marked Secure whenever the client is on HTTPS — agent TLS, a
forwarded https scheme under --trust-proxy, or a direct TLS connection. It is not marked
Secure on a plain-HTTP origin, because a cookie the browser throws away is not a hardening
win: the login would succeed and the UI would say "unauthorized".
A gap to know about#
Check it worked#
Sign in through the button, on an account you have not created locally, and confirm:
- an account appeared with the name from the username claim;
- its role matches the group you put the person in;
- the audit log attributes their next change to that name.
Administration → Single sign-on shows provider state, the group mapping and every linked account.
R=https://<router>:8443
T=wh_... # an admin token
curl -sk -H "Authorization: Bearer $T" "$R/api/admin/oidc"Then remove somebody from a group at the IdP and confirm their role changes at the next sign-in. That is the property you configured this for; test it once.
Undoing it#
Remove the drop-in and restart:
sudo rm /etc/systemd/system/wheelhouse-agent.service.d/oidc.conf
sudo systemctl daemon-reload
sudo systemctl restart wheelhouse-agentProvisioned accounts stay. Give the people who need them a password, or delete the accounts — Add an operator.
A person can detach their own account with POST /api/auth/oidc/unlink, which is refused if
it would leave them with no way in.
See also#
Checked against agent/oidc.go ·
agent/main.go ·
docs/deploy.md ·
docs/adr/002-oidc-client.md ·
docs/security.md