docs/ref/oidc.md
Headscale supports authentication via external identity providers using OpenID Connect (OIDC). It features:
Please see limitations for known issues and limitations.
OpenID requires configuration in Headscale and your identity provider:
oidc section of the Headscale configuration contains all available configuration
options along with a description and their default values.A basic configuration connects Headscale to an identity provider and typically requires:
https://sso.example.com).headscale).generated-secret).https://headscale.example.com/oidc/callback).=== "Headscale"
```yaml
oidc:
issuer: "https://sso.example.com"
client_id: "headscale"
client_secret: "generated-secret"
```
=== "Identity provider"
- Create a new confidential client (`Client ID`, `Client secret`)
- Add Headscale's OIDC callback URL as valid redirect URL: `https://headscale.example.com/oidc/callback`
- Configure additional parameters to improve user experience such as: name, description, logo, …
Proof Key for Code Exchange (PKCE) adds an additional layer of security to the OAuth 2.0 authorization code flow by preventing authorization code interception attacks, see: https://datatracker.ietf.org/doc/html/rfc7636. PKCE is recommended and needs to be configured for Headscale and the identity provider alike:
=== "Headscale"
```yaml hl_lines="5-6"
oidc:
issuer: "https://sso.example.com"
client_id: "headscale"
client_secret: "generated-secret"
pkce:
enabled: true
```
=== "Identity provider"
- Enable PKCE for the headscale client
- Set the PKCE challenge method to "S256"
Headscale allows to filter for allowed users based on their domain, email address or group membership. These filters can be helpful to apply additional restrictions and control which users are allowed to join. Filters are disabled by default, users are allowed to join once the authentication with the identity provider succeeds. In case multiple filters are configured, a user needs to pass all of them.
=== "Allowed domains"
- Check the email domain of each authenticating user against the list of allowed domains and only authorize users
whose email domain matches `example.com`.
- A verified email address is required [unless email verification is disabled](#control-email-verification).
- Access allowed: `[email protected]`
- Access denied: `[email protected]`
```yaml hl_lines="5-6"
oidc:
issuer: "https://sso.example.com"
client_id: "headscale"
client_secret: "generated-secret"
allowed_domains:
- "example.com"
```
=== "Allowed users/emails"
- Check the email address of each authenticating user against the list of allowed email addresses and only authorize
users whose email is part of the `allowed_users` list.
- A verified email address is required [unless email verification is disabled](#control-email-verification).
- Access allowed: `[email protected]`, `[email protected]`
- Access denied: `[email protected]`
```yaml hl_lines="5-7"
oidc:
issuer: "https://sso.example.com"
client_id: "headscale"
client_secret: "generated-secret"
allowed_users:
- "[email protected]"
- "[email protected]"
```
=== "Allowed groups"
- Use the OIDC `groups` claim of each authenticating user to get their group membership and only authorize users
which are members in at least one of the referenced groups.
- Access allowed: users in the `headscale_users` group
- Access denied: users without groups, users with other groups
```yaml hl_lines="5-7"
oidc:
issuer: "https://sso.example.com"
client_id: "headscale"
client_secret: "generated-secret"
scope: ["openid", "profile", "email", "groups"]
allowed_groups:
- "headscale_users"
```
Headscale uses the email claim from the identity provider to synchronize the email address to its user profile. By
default, a user's email address is only synchronized when the identity provider reports the email address as verified
via the email_verified: true claim.
Unverified emails may be allowed in case an identity provider does not send the email_verified claim or email
verification is not required. In that case, a user's email address is always synchronized to the user profile.
oidc:
issuer: "https://sso.example.com"
client_id: "headscale"
client_secret: "generated-secret"
email_verified_required: false
The node expiration is the amount of time a node is authenticated with OpenID Connect until it expires and needs to reauthenticate. The default node expiration is 180 days. This can either be customized or set to the expiration from the Access Token.
=== "Customize node expiration"
```yaml hl_lines="5"
oidc:
issuer: "https://sso.example.com"
client_id: "headscale"
client_secret: "generated-secret"
expiry: 30d # Use 0 to disable node expiration
```
=== "Use expiration from Access Token"
Please keep in mind that the Access Token is typically a short-lived token that expires within a few minutes. You
will have to configure token expiration in your identity provider to avoid frequent re-authentication.
```yaml hl_lines="5"
oidc:
issuer: "https://sso.example.com"
client_id: "headscale"
client_secret: "generated-secret"
use_expiry_from_token: true
```
!!! tip "Expire a node and force re-authentication"
A node can be expired immediately via:
```console
headscale node expire -i <NODE_ID>
```
You may refer to users in the Headscale policy via:
!!! note "A user identifier in the policy must contain a single @"
The Headscale policy requires a single `@` to reference a user. If the username or provider identifier doesn't
already contain a single `@`, it needs to be appended at the end. For example: the username `ssmith` has to be
written as `ssmith@` to be correctly identified as user within the policy.
!!! warning "Email address or username might be updated by users"
Many identity providers allow users to update their own profile. Depending on the identity provider and its
configuration, the values for username or email address might change over time. This might have unexpected
consequences for Headscale where a policy might no longer work or a user might obtain more access by hijacking an
existing username or email address.
!!! tip "Howto use the provider identifier in the policy"
The provider identifier uniquely identifies an OIDC user and a well-behaving identity provider guarantees that this
value never changes for a particular user. It is usually an opaque and long string and its value is currently only
available from the [API](api.md), database or directly from your identity provider).
Use the [API](api.md) with the `/api/v1/user` endpoint to fetch the provider identifier (`providerId`). The value
(be sure to append an `@` in case the provider identifier doesn't already contain an `@` somewhere) can be used
directly to reference a user in the policy. To improve readability of the policy, one may use the `groups` section
as an alias:
```json
{
"groups": {
"group:alice": [
"https://soo.example.com/oauth2/openid/59ac9125-c31b-46c5-814e-06242908cf57@"
]
},
"acls": [
{
"action": "accept",
"src": ["group:alice"],
"dst": ["*:*"]
}
]
}
```
Headscale uses the standard OIDC claims to populate and update its local user profile on each login. OIDC claims are read from the ID Token and from the UserInfo endpoint.
| Headscale profile | OIDC claim | Notes / examples |
|---|---|---|
| email address | email | Only verified emails are synchronized, unless email_verified_required: false is configured |
| display name | name | eg: Sam Smith |
| username | preferred_username | Depends on identity provider, eg: ssmith, [email protected], \\example.com\ssmith |
| profile picture | picture | URL to a profile picture or avatar |
| provider identifier | iss, sub | A stable and unique identifier for a user, typically a combination of iss and sub OIDC claims |
groups | Only used to filter for allowed groups |
@.Please see the GitHub label "OIDC" for OIDC related issues.
!!! warning "Third-party software and services"
This section of the documentation is specific for third-party software and services. We recommend users read the
third-party documentation on how to configure and integrate an OIDC client. Please see the [Configuration
section](#configuration) for a description of Headscale's OIDC related configuration settings.
Any identity provider with OpenID Connect support should "just work" with Headscale. The following identity providers are known to work:
Authelia is fully supported by Headscale.
Encryption Key in the providers section unset.!!! warning "No username due to missing preferred_username"
Google OAuth does not send the `preferred_username` claim when the scope `profile` is requested. The username in
Headscale will be blank/not set.
In order to integrate Headscale with Google, you'll need to have a Google Cloud Console account.
Google OAuth has a verification process if you need to have
users authenticate who are outside of your domain. If you only need to authenticate users from your domain name (ie
@example.com), you don't need to go through the verification process.
However if you don't have a domain, or need to add users outside of your domain, you can manually add emails via Google Console.
APIs and services -> CredentialsCreate Credentials -> OAuth client IDApplication Type, choose Web ApplicationName, enter whatever you likeAuthorised redirect URIs, add Headscale's OIDC callback URL: https://headscale.example.com/oidc/callbackSave at the bottom of the formClient ID and Client secret, you can also download it for reference if you need it.https://accounts.google.com.[email protected].[email protected]) as preferred_username by default. Headscale stores this value as
username which might be confusing as the username and email fields now contain values that look like an email address.
Kanidm can be configured to send the short username as preferred_username attribute
instead:
kanidm system oauth2 prefer-short-username <client name>
alice and can be referred to as alice@ in the policy.Keycloak is fully supported by Headscale.
Keycloak has no built-in client scope for the OIDC groups claim. This extra configuration step is only needed if
you need to authorize access based on group membership.
groups for OpenID Connect:
Group Membership mapper with name groups and the token claim name groups.group.Default.Full group path option:
Full group path is enabled: groups contain their full path, e.g. /top/group1Full group path is disabled: only the name of the group is used, e.g. group1In order to integrate Headscale with Microsoft Entra ID, you'll need to provision an App Registration with the correct scopes and redirect URI.
Configure Headscale following the "Basic configuration" steps. The issuer URL for Microsoft
Entra ID is: https://login.microsoftonline.com/<tenant-UUID>/v2.0. The following extra_params might be useful:
domain_hint: example.com to use your own domainprompt: select_account to force an account picker during loginWhen using Microsoft Entra ID together with the allowed groups filter, configure the
Headscale OIDC scope without the groups claim, for example:
oidc:
scope: ["openid", "profile", "email"]
Groups for the allowed groups filter need to be specified with their group ID(UUID) instead of the group name.
Headscale only supports a single OIDC provider in its configuration, but it does store the provider identifier of each user. When switching providers, this might lead to issues with existing users: all user details (name, email, groups) might be identical with the new provider, but the identifier will differ. Headscale will be unable to create a new user as the name and email will already be in use for the existing users.
At this time, you will need to manually update the provider_identifier column in the users table for each user with the appropriate value for the new provider. The identifier is built from the iss and sub claims of the OIDC ID token, for example https://id.example.com/12340987.