mirror of
https://github.com/tale/headplane.git
synced 2026-07-27 00:08:14 +00:00
357 lines
14 KiB
Markdown
357 lines
14 KiB
Markdown
---
|
|
title: Single Sign-On (SSO)
|
|
description: Configure Single Sign-On (SSO) authentication for Headplane.
|
|
outline: [2, 3]
|
|
---
|
|
|
|
# Single Sign-On (SSO)
|
|
|
|
<figure>
|
|
<img class="dark-only" src="../assets/sso-dark.png" />
|
|
<img class="light-only" src="../assets/sso-light.png" />
|
|
<figcaption>SSO Configuration Page</figcaption>
|
|
</figure>
|
|
|
|
Single Sign-On allows users to authenticate with Headplane through an external
|
|
Identity Provider (IdP) using the OpenID Connect (OIDC) protocol. When enabled,
|
|
users sign in through your IdP and Headplane automatically links them to their
|
|
Headscale identity, assigns a role, and manages their session.
|
|
|
|
If your reverse proxy already performs authentication and can pass trusted user
|
|
headers to Headplane, see [Proxy Authentication](./proxy-auth.md) instead.
|
|
|
|
## Getting Started
|
|
|
|
### Requirements
|
|
|
|
You'll need the following before proceeding:
|
|
|
|
- A working Headplane installation that is already configured.
|
|
- An Identity Provider (IdP) that supports OAuth2 and OpenID Connect (OIDC).
|
|
- `server.base_url` set to the public URL of your Headplane instance in your
|
|
configuration file (the domain visible in the browser).
|
|
- A Headscale API key with a relatively long expiration time (eg. 1 year).
|
|
|
|
### Configuring the Client
|
|
|
|
You'll need to create a client in your Identity Provider that Headplane can use
|
|
for authentication. As part of that step, you'll need to register a "redirect
|
|
URL" — this is where the IdP sends users after they authenticate.
|
|
|
|
For Headplane, the redirect URL will be in the following format (replace the
|
|
domain with the value set for `server.base_url`):
|
|
|
|
```
|
|
https://headplane.example.com/admin/oidc/callback
|
|
```
|
|
|
|
Once you have created the client, make note of the following:
|
|
|
|
- Client ID
|
|
- Client Secret (if applicable)
|
|
- Issuer URL
|
|
|
|
### OIDC Configuration
|
|
|
|
To enable OIDC authentication in Headplane, add the following to your
|
|
configuration file:
|
|
|
|
```yaml
|
|
headscale:
|
|
url: "http://headscale:8080"
|
|
api_key: "<generated-api-key>"
|
|
|
|
oidc:
|
|
issuer: "https://your-idp.com"
|
|
client_id: "your-client-id"
|
|
client_secret: "your-client-secret"
|
|
# You can also provide the client secret via a file:
|
|
# client_secret_path: "${HOME}/secrets/headplane_oidc_client_secret.txt"
|
|
|
|
# These are usually auto-discovered, but can be set manually:
|
|
# authorization_endpoint: ""
|
|
# token_endpoint: ""
|
|
# userinfo_endpoint: ""
|
|
# scope: "openid email profile"
|
|
# subject_claims: ["open_id", "email"]
|
|
# default_role: "member"
|
|
# role_claim: "headplane_role"
|
|
# allow_weak_rsa_keys: false
|
|
# extra_params:
|
|
# foo: "bar"
|
|
```
|
|
|
|
Headplane automatically discovers OIDC endpoints from your issuer's
|
|
`/.well-known/openid-configuration`. If your IdP does not support discovery,
|
|
you'll need to set the endpoints manually.
|
|
|
|
### Non-standard Subject Claims
|
|
|
|
Some providers do not return the standard OIDC `sub` claim in the ID token.
|
|
Headplane always uses `sub` first, but you can configure fallback claims with
|
|
`oidc.subject_claims`.
|
|
|
|
For Feishu/Lark, the recommended configuration is:
|
|
|
|
```yaml
|
|
oidc:
|
|
subject_claims: ["open_id", "email"]
|
|
```
|
|
|
|
This keeps identity matching stable by preferring `open_id` and only falling
|
|
back to `email` if needed.
|
|
|
|
### Legacy Weak RSA Signing Keys
|
|
|
|
Some legacy providers still sign ID tokens with RSA keys smaller than 2048
|
|
bits. Headplane rejects those keys by default.
|
|
|
|
If your provider cannot rotate to a stronger signing key yet, you can
|
|
explicitly enable the compatibility fallback:
|
|
|
|
```yaml
|
|
oidc:
|
|
allow_weak_rsa_keys: true
|
|
```
|
|
|
|
::: warning
|
|
This weakens ID token verification security and should only be used as a
|
|
temporary workaround while your provider rotates to a 2048-bit-or-larger key.
|
|
:::
|
|
|
|
### PKCE
|
|
|
|
::: warning
|
|
Headplane currently only supports the **`S256`** code challenge method for PKCE.
|
|
You may need to ensure that your Identity Provider is configured to accept this
|
|
method.
|
|
:::
|
|
|
|
By default, Headplane does not use PKCE (Proof Key for Code Exchange). PKCE is
|
|
a best practice for OIDC and enhances security — some IdPs even require it. To
|
|
enable PKCE:
|
|
|
|
```yaml
|
|
oidc:
|
|
use_pkce: true
|
|
```
|
|
|
|
## How User Matching Works
|
|
|
|
When a user signs in via OIDC, Headplane needs to link them to their
|
|
corresponding Headscale user. This is important for features like showing a
|
|
user's own machines, self-service pre-auth keys, and WebSSH.
|
|
|
|
### Matching Strategy
|
|
|
|
Headplane uses a two-step matching strategy:
|
|
|
|
1. **Subject match (primary)**: Headscale stores the IdP's `provider_id` for
|
|
each OIDC user (e.g. `https://idp.example.com/3d6f6e3f-...`). Headplane
|
|
extracts the last path segment and compares it to the resolved OIDC subject.
|
|
The resolved subject uses `sub` first, then falls back to any configured
|
|
`oidc.subject_claims`. If they match, the user is linked.
|
|
|
|
2. **Email match (fallback)**: If the subject doesn't match, Headplane falls
|
|
back to comparing the user's email address from the OIDC `userinfo` endpoint
|
|
against the email stored on the Headscale user record.
|
|
|
|
Once a link is established, it's stored as a `headscale_user_id` in Headplane's
|
|
database and reused on subsequent logins — so the matching only needs to succeed
|
|
once.
|
|
|
|
### Headscale Without OIDC
|
|
|
|
If your Headscale instance uses **local users** (created via
|
|
`headscale users create`) rather than OIDC, automatic matching cannot work —
|
|
local users have no `provider_id` or email to compare against.
|
|
|
|
In this case, Headplane will prompt the user during onboarding to manually
|
|
select which Headscale user they are. This selection is persisted, so it only
|
|
needs to happen once. After linking, all ownership-based features (viewing your
|
|
own machines, self-service pre-auth keys, WebSSH) work normally.
|
|
|
|
::: tip
|
|
If you skip the user selection during onboarding, you can still use Headplane
|
|
— you just won't have ownership-based features. An admin can manage everything
|
|
regardless of whether users are linked.
|
|
:::
|
|
|
|
### Same Client vs. Different Clients
|
|
|
|
::: tip Recommended
|
|
Using the **same OIDC client** for both Headscale and Headplane is the simplest
|
|
and most reliable setup. The `sub` claim will be identical for both services,
|
|
so subject matching always works.
|
|
:::
|
|
|
|
If your Headscale and Headplane use **different OIDC clients**, some Identity
|
|
Providers (notably Azure AD / Entra ID) may issue different `sub` values per
|
|
client application. In this case:
|
|
|
|
- Subject matching will fail on the first login.
|
|
- Headplane will fall back to email matching, which requires that the `email`
|
|
claim is available from both your IdP's `userinfo` endpoint and Headscale's
|
|
user record.
|
|
- Once the link is established, subsequent logins will work regardless because
|
|
the link is persisted.
|
|
|
|
::: warning
|
|
If you use different clients **and** your IdP does not provide an `email` claim,
|
|
Headplane will not be able to match users to their Headscale identity. Users
|
|
will still be able to sign in, but they won't be linked to a Headscale user —
|
|
meaning features like viewing their own machines or self-service pre-auth keys
|
|
won't work.
|
|
:::
|
|
|
|
## Roles and Permissions
|
|
|
|
When SSO is enabled, Headplane uses a role-based access control system to
|
|
determine what each user can do in the UI.
|
|
|
|
### Available Roles
|
|
|
|
| Role | Description |
|
|
| ----------------- | ------------------------------------------------------------------------------------------------------ |
|
|
| **Owner** | Full access to everything. Cannot be reassigned. Automatically granted to the first user who signs in. |
|
|
| **Admin** | Full access except the owner-specific flag. Can manage all users, machines, ACLs, DNS, and settings. |
|
|
| **Network Admin** | Can manage ACLs, DNS, and network settings. Can view machines and users. Can generate pre-auth keys. |
|
|
| **IT Admin** | Can manage machines, users, and feature settings. Can configure IAM. Cannot modify ACLs or DNS. |
|
|
| **Auditor** | Read-only access to everything. Can generate their own pre-auth keys. |
|
|
| **Viewer** | Can view machines and users. Can generate their own pre-auth keys. |
|
|
| **Member** | No UI access. The user exists in Headplane's database but has not been granted any permissions. |
|
|
|
|
### First Login (Owner Bootstrap)
|
|
|
|
The very first user to sign in via OIDC is automatically assigned the **Owner**
|
|
role. All subsequent users are assigned the **Member** role (no access) by
|
|
default. An owner or admin must then assign them an appropriate role through
|
|
the Users page.
|
|
|
|
### Automatic Role Assignment
|
|
|
|
You can change the role assigned to newly created OIDC users with
|
|
`oidc.default_role`:
|
|
|
|
```yaml
|
|
oidc:
|
|
# Valid values: admin, network_admin, it_admin, auditor, viewer, member
|
|
default_role: "viewer"
|
|
```
|
|
|
|
This is useful when Headscale already restricts who can authenticate by domain,
|
|
group, or user. For example, if Headscale only allows `@example.com` users to
|
|
sign in and all of those users should be able to view Headplane, set
|
|
`default_role: "viewer"`.
|
|
|
|
For per-user roles from your IdP, configure `oidc.role_claim` with the OIDC
|
|
claim that contains a Headplane role:
|
|
|
|
```yaml
|
|
oidc:
|
|
role_claim: "headplane_role"
|
|
```
|
|
|
|
The claim may be a string, such as `"admin"`, or an array containing one of the
|
|
valid roles. This lets providers such as Keycloak map groups or client roles to
|
|
a final Headplane role before login. When both `role_claim` and `default_role`
|
|
are configured, a valid role claim takes precedence for new users.
|
|
|
|
For users that already exist in Headplane, a valid `role_claim` is synced on
|
|
each OIDC login. If their IdP groups or client roles start matching a different
|
|
Headplane role, their Headplane permissions are updated at their next sign-in.
|
|
`default_role` remains a creation-time fallback only and does not overwrite
|
|
existing roles. The **Owner** role is reserved for the first-login bootstrap and
|
|
cannot be granted or overwritten by `default_role` or `role_claim`.
|
|
|
|
### API Key Sessions
|
|
|
|
Users who sign in with a Headscale API key (instead of OIDC) are treated as
|
|
having full access. API key sessions bypass the role system entirely since
|
|
possession of the API key already implies administrative access to Headscale.
|
|
|
|
### Onboarding
|
|
|
|
When a new OIDC user signs in for the first time, they go through a brief
|
|
onboarding flow that helps them connect their first device to the Tailnet. This
|
|
flow can be skipped. Once completed, users are taken to the main dashboard.
|
|
|
|
## Single Logout (RP-Initiated Logout)
|
|
|
|
Headplane supports
|
|
[OpenID Connect RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html).
|
|
When enabled, clicking "Log Out" in the UI from an OIDC-backed session will:
|
|
|
|
1. Destroy the local Headplane session.
|
|
2. Redirect the browser to the identity provider's `end_session_endpoint`.
|
|
3. Pass along the original `id_token` as `id_token_hint`, plus a
|
|
`post_logout_redirect_uri` so the IdP can return the user to Headplane after
|
|
it has cleared its own session.
|
|
|
|
### Configuration
|
|
|
|
This feature is **disabled by default** because the `post_logout_redirect_uri`
|
|
must be pre-registered in your OIDC client on the IdP. Enabling it without that
|
|
registration will land users on the provider's error page after logout.
|
|
|
|
To enable it, set `oidc.use_end_session: true`:
|
|
|
|
```yaml
|
|
oidc:
|
|
# Required: opt in to RP-initiated logout
|
|
use_end_session: true
|
|
|
|
# Optional: override the auto-discovered end_session_endpoint, or set it
|
|
# manually if your provider does not expose it via discovery.
|
|
# end_session_endpoint: "https://idp.example.com/realms/main/protocol/openid-connect/logout"
|
|
|
|
# Optional. Defaults to `<server.base_url>/admin/login?s=logout`.
|
|
# post_logout_redirect_uri: "https://headplane.example.com/admin/login?s=logout"
|
|
```
|
|
|
|
If your provider exposes `end_session_endpoint` in its discovery document
|
|
(Keycloak, Authentik, Auth0, Azure AD, …) Headplane picks it up automatically
|
|
once `use_end_session` is `true`.
|
|
|
|
::: tip
|
|
Make sure the redirect URI you supply (or the default one Headplane builds) is
|
|
listed under the post-logout / valid redirect URIs in your IdP's client
|
|
configuration, otherwise the provider will refuse to redirect back.
|
|
:::
|
|
|
|
When `use_end_session` is `false` (the default), Headplane simply destroys its
|
|
own session and returns the user to the login page.
|
|
|
|
## Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
- **"OIDC is not enabled or misconfigured"**: Check that your `oidc` section
|
|
is present in the config and that the issuer URL is reachable from the
|
|
Headplane server.
|
|
|
|
- **User signs in but can't see their machines**: The user's Headscale identity
|
|
wasn't matched. Check that either the `sub` claim matches or the `email`
|
|
claim is available (see [How User Matching Works](#how-user-matching-works)).
|
|
|
|
- **"Session cookie is empty" or login loop**: Check your `cookie_secure`
|
|
setting. If Headplane is behind a reverse proxy with HTTPS, set it to `true`.
|
|
If running without HTTPS (eg. local development), set it to `false`.
|
|
|
|
- **Invalid API Key**: The `headscale.api_key` may have expired. Generate
|
|
a new one with `headscale apikeys create --expiration 999d`.
|
|
|
|
- **Missing the `sub` claim**: If your IdP omits `sub`, configure
|
|
`oidc.subject_claims` with a stable fallback such as `open_id`. Only use
|
|
`email` as a fallback when it is stable for your users.
|
|
|
|
- **Redirect URI Mismatch**: Ensure the redirect URI registered in your IdP
|
|
matches `{server.base_url}/admin/oidc/callback` exactly.
|
|
|
|
- **PKCE errors**: If your IdP requires PKCE, set `oidc.use_pkce: true`. If
|
|
you see errors mentioning `code_verifier`, this is almost always the cause.
|
|
|
|
- **Missing endpoints**: If your IdP does not support OIDC discovery, you'll
|
|
need to set `authorization_endpoint`, `token_endpoint`, and
|
|
`userinfo_endpoint` manually in the config.
|