2025-06-24 08:20:15 +02:00
|
|
|
# OpenID Connect
|
|
|
|
|
|
|
|
Headscale supports authentication via external identity providers using OpenID Connect (OIDC). It features:
|
|
|
|
|
|
|
|
- Autoconfiguration via OpenID Connect Discovery Protocol
|
|
|
|
- [Proof Key for Code Exchange (PKCE) code verification](#enable-pkce-recommended)
|
|
|
|
- [Authorization based on a user's domain, email address or group membership](#authorize-users-with-filters)
|
|
|
|
- Synchronization of [standard OIDC claims](#supported-oidc-claims)
|
|
|
|
|
|
|
|
Please see [limitations](#limitations) for known issues and limitations.
|
|
|
|
|
|
|
|
## Configuration
|
|
|
|
|
|
|
|
OpenID requires configuration in Headscale and your identity provider:
|
|
|
|
|
|
|
|
- Headscale: The `oidc` section of the Headscale [configuration](configuration.md) contains all available configuration
|
|
|
|
options along with a description and their default values.
|
|
|
|
- Identity provider: Please refer to the official documentation of your identity provider for specific instructions.
|
|
|
|
Additionally, there might be some useful hints in the [Identity provider specific
|
|
|
|
configuration](#identity-provider-specific-configuration) section below.
|
|
|
|
|
|
|
|
### Basic configuration
|
|
|
|
|
|
|
|
A basic configuration connects Headscale to an identity provider and typically requires:
|
|
|
|
|
|
|
|
- OpenID Connect Issuer URL from the identity provider. Headscale uses the OpenID Connect Discovery Protocol 1.0 to
|
|
|
|
automatically obtain OpenID configuration parameters (example: `https://sso.example.com`).
|
|
|
|
- Client ID from the identity provider (example: `headscale`).
|
|
|
|
- Client secret generated by the identity provider (example: `generated-secret`).
|
|
|
|
- Redirect URI for your identity provider (example: `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, …
|
|
|
|
|
|
|
|
### Enable PKCE (recommended)
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
|
|
### Authorize users with filters
|
|
|
|
|
|
|
|
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`.
|
|
|
|
* Access allowed: `alice@example.com`
|
|
|
|
* Access denied: `bob@example.net`
|
|
|
|
|
|
|
|
```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.
|
|
|
|
* Access allowed: `alice@example.com`, `bob@example.net`
|
|
|
|
* Access denied: `mallory@example.net`
|
|
|
|
|
|
|
|
```yaml hl_lines="5-7"
|
|
|
|
oidc:
|
|
|
|
issuer: "https://sso.example.com"
|
|
|
|
client_id: "headscale"
|
|
|
|
client_secret: "generated-secret"
|
|
|
|
allowed_users:
|
|
|
|
- "alice@example.com"
|
|
|
|
- "bob@example.net"
|
|
|
|
```
|
|
|
|
|
|
|
|
=== "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"
|
|
|
|
```
|
|
|
|
|
|
|
|
### Customize node expiration
|
|
|
|
|
|
|
|
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 reauthentication.
|
|
|
|
|
|
|
|
|
|
|
|
```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>
|
|
|
|
```
|
|
|
|
|
|
|
|
### Reference a user in the policy
|
|
|
|
|
|
|
|
You may refer to users in the Headscale policy via:
|
|
|
|
|
|
|
|
- Email address
|
|
|
|
- Username
|
|
|
|
- Provider identifier (only available in the database or from your identity provider)
|
|
|
|
|
|
|
|
!!! 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.
|
|
|
|
|
|
|
|
## Supported OIDC claims
|
|
|
|
|
|
|
|
Headscale uses [the standard OIDC claims](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) to
|
|
|
|
populate and update its local user profile on each login. OIDC claims are read from the ID Token or from the UserInfo
|
|
|
|
endpoint.
|
|
|
|
|
|
|
|
| Headscale profile | OIDC claim | Notes / examples |
|
|
|
|
| ------------------- | -------------------- | ------------------------------------------------------------------------------------------------- |
|
|
|
|
| email address | `email` | Only used when `email_verified: true` |
|
|
|
|
| display name | `name` | eg: `Sam Smith` |
|
|
|
|
| username | `preferred_username` | Depends on identity provider, eg: `ssmith`, `ssmith@idp.example.com`, `\\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](#authorize-users-with-filters) |
|
|
|
|
|
|
|
|
## Limitations
|
|
|
|
|
|
|
|
- Support for OpenID Connect aims to be generic and vendor independent. It offers only limited support for quirks of
|
|
|
|
specific identity providers.
|
|
|
|
- OIDC groups cannot be used in ACLs.
|
|
|
|
- The username provided by the identity provider needs to adhere to this pattern:
|
|
|
|
- The username must be at least two characters long.
|
|
|
|
- It must only contain letters, digits, hyphens, dots, underscores, and up to a single `@`.
|
|
|
|
- The username must start with a letter.
|
|
|
|
- A user's email address is only synchronized to the local user profile when the identity provider marks the email
|
|
|
|
address as verified (`email_verified: true`).
|
|
|
|
|
|
|
|
Please see the [GitHub label "OIDC"](https://github.com/juanfont/headscale/labels/OIDC) for OIDC related issues.
|
|
|
|
|
|
|
|
## Identity provider specific configuration
|
|
|
|
|
|
|
|
!!! 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](#authelia)
|
|
|
|
- [Authentik](#authentik)
|
|
|
|
- [Kanidm](#kanidm)
|
|
|
|
- [Keycloak](#keycloak)
|
|
|
|
|
|
|
|
### Authelia
|
|
|
|
|
|
|
|
Authelia is fully supported by Headscale.
|
|
|
|
|
|
|
|
#### Additional configuration to authorize users based on filters
|
|
|
|
|
|
|
|
Authelia (4.39.0 or newer) no longer provides standard OIDC claims such as `email` or `groups` via the ID Token. The
|
|
|
|
OIDC `email` and `groups` claims are used to [authorize users with filters](#authorize-users-with-filters). This extra
|
|
|
|
configuration step is **only** needed if you need to authorize access based on one of the following user properties:
|
|
|
|
|
|
|
|
- domain
|
|
|
|
- email address
|
|
|
|
- group membership
|
|
|
|
|
|
|
|
Please follow the instructions from Authelia's documentation on how to [Restore Functionality Prior to Claims
|
|
|
|
Parameter](https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter).
|
|
|
|
|
|
|
|
### Authentik
|
|
|
|
|
|
|
|
- Authentik is fully supported by Headscale.
|
|
|
|
- [Headscale does not JSON Web Encryption](https://github.com/juanfont/headscale/issues/2446). Leave the field
|
|
|
|
`Encryption Key` in the providers section unset.
|
|
|
|
|
|
|
|
### Google OAuth
|
|
|
|
|
|
|
|
!!! 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](https://console.cloud.google.com) account.
|
|
|
|
|
|
|
|
Google OAuth has a [verification process](https://support.google.com/cloud/answer/9110914?hl=en) 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.
|
|
|
|
|
|
|
|
#### Steps
|
2023-02-27 16:36:40 +08:00
|
|
|
|
2023-02-22 21:06:53 +08:00
|
|
|
1. Go to [Google Console](https://console.cloud.google.com) and login or create an account if you don't have one.
|
|
|
|
2. Create a project (if you don't already have one).
|
|
|
|
3. On the left hand menu, go to `APIs and services` -> `Credentials`
|
|
|
|
4. Click `Create Credentials` -> `OAuth client ID`
|
|
|
|
5. Under `Application Type`, choose `Web Application`
|
|
|
|
6. For `Name`, enter whatever you like
|
2025-06-24 08:20:15 +02:00
|
|
|
7. Under `Authorised redirect URIs`, add Headscale's OIDC callback URL: `https://headscale.example.com/oidc/callback`
|
2023-02-22 21:06:53 +08:00
|
|
|
8. Click `Save` at the bottom of the form
|
|
|
|
9. Take note of the `Client ID` and `Client secret`, you can also download it for reference if you need it.
|
2025-06-24 08:20:15 +02:00
|
|
|
10. [Configure Headscale following the "Basic configuration" steps](#basic-configuration). The issuer URL for Google
|
|
|
|
OAuth is: `https://accounts.google.com`.
|
|
|
|
|
|
|
|
### Kanidm
|
|
|
|
|
|
|
|
- Kanidm is fully supported by Headscale.
|
|
|
|
- Groups for the [allowed groups filter](#authorize-users-with-filters) need to be specified with their full SPN, for
|
|
|
|
example: `headscale_users@sso.example.com`.
|
|
|
|
|
|
|
|
### Keycloak
|
|
|
|
|
|
|
|
Keycloak is fully supported by Headscale.
|
|
|
|
|
|
|
|
#### Additional configuration to use the allowed groups filter
|
|
|
|
|
|
|
|
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](#authorize-users-with-filters).
|
|
|
|
|
|
|
|
- Create a new client scope `groups` for OpenID Connect:
|
|
|
|
- Configure a `Group Membership` mapper with name `groups` and the token claim name `groups`.
|
|
|
|
- Enable the mapper for the ID Token, Access Token and UserInfo endpoint.
|
|
|
|
- Configure the new client scope for your Headscale client:
|
|
|
|
- Edit the Headscale client.
|
|
|
|
- Search for the client scope `group`.
|
|
|
|
- Add it with assigned type `Default`.
|
|
|
|
- [Configure the allowed groups in Headscale](#authorize-users-with-filters). Keep in mind that groups in Keycloak start
|
|
|
|
with a leading `/`.
|
|
|
|
|
|
|
|
### Microsoft Entra ID
|
|
|
|
|
|
|
|
In 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](#basic-configuration). The issuer URL for Microsoft
|
|
|
|
Entra ID is: `https://login.microsoftonline.com/<tenant-UUID>/v2.0`. The following `extra_params` might be useful:
|
2023-02-22 21:06:53 +08:00
|
|
|
|
2025-06-24 08:20:15 +02:00
|
|
|
- `domain_hint: example.com` to use your own domain
|
|
|
|
- `prompt: select_account` to force an account picker during login
|