mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			318 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			318 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # OpenID Connect
 | |
| 
 | |
| Headscale supports authentication via external identity providers using OpenID Connect (OIDC). It features:
 | |
| 
 | |
| - Auto configuration 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 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>
 | |
|     ```
 | |
| 
 | |
| ### 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
 | |
| 
 | |
| 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
 | |
| 7. Under `Authorised redirect URIs`, add Headscale's OIDC callback URL: `https://headscale.example.com/oidc/callback`
 | |
| 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.
 | |
| 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:
 | |
| 
 | |
| - `domain_hint: example.com` to use your own domain
 | |
| - `prompt: select_account` to force an account picker during login
 |