diff --git a/config-example.yaml b/config-example.yaml index b62ca02e..1c562ebd 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -319,51 +319,60 @@ dns: # Note: for production you will want to set this to something like: unix_socket: /var/run/headscale/headscale.sock unix_socket_permission: "0770" -# -# headscale supports experimental OpenID connect support, -# it is still being tested and might have some bugs, please -# help us test it. + # OpenID Connect # oidc: +# # Block startup until the identity provider is available and healthy. # only_start_if_oidc_is_available: true +# +# # OpenID Connect Issuer URL from the identity provider # issuer: "https://your-oidc.issuer.com/path" +# +# # Client ID from the identity provider # client_id: "your-oidc-client-id" +# +# # Client secret generated by the identity provider +# # Note: client_secret and client_secret_path are mutually exclusive. # client_secret: "your-oidc-client-secret" # # Alternatively, set `client_secret_path` to read the secret from the file. # # It resolves environment variables, making integration to systemd's # # `LoadCredential` straightforward: # client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret" -# # client_secret and client_secret_path are mutually exclusive. # -# # The amount of time from a node is authenticated with OpenID until it -# # expires and needs to reauthenticate. +# # The amount of time a node is authenticated with OpenID until it expires +# # and needs to reauthenticate. # # Setting the value to "0" will mean no expiry. # expiry: 180d # # # Use the expiry from the token received from OpenID when the user logged -# # in, this will typically lead to frequent need to reauthenticate and should -# # only been enabled if you know what you are doing. +# # in. This will typically lead to frequent need to reauthenticate and should +# # only be enabled if you know what you are doing. # # Note: enabling this will cause `oidc.expiry` to be ignored. # use_expiry_from_token: false # -# # Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query -# # parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email". +# # The OIDC scopes to use, defaults to "openid", "profile" and "email". +# # Custom scopes can be configured as needed, be sure to always include the +# # required "openid" scope. +# scope: ["openid", "profile", "email"] # -# scope: ["openid", "profile", "email", "custom"] +# # Provide custom key/value pairs which get sent to the identity provider's +# # authorization endpoint. # extra_params: # domain_hint: example.com # -# # List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the -# # authentication request will be rejected. -# +# # Only accept users whose email domain is part of the allowed_domains list. # allowed_domains: # - example.com -# # Note: Groups from keycloak have a leading '/' -# allowed_groups: -# - /headscale +# +# # Only accept users whose email address is part of the allowed_users list. # allowed_users: # - alice@example.com # +# # Only accept users which are members of at least one group in the +# # allowed_groups list. +# allowed_groups: +# - /headscale +# # # Optional: PKCE (Proof Key for Code Exchange) configuration # # PKCE adds an additional layer of security to the OAuth 2.0 authorization code flow # # by preventing authorization code interception attacks @@ -371,6 +380,7 @@ unix_socket_permission: "0770" # pkce: # # Enable or disable PKCE support (default: false) # enabled: false +# # # PKCE method to use: # # - plain: Use plain code verifier # # - S256: Use SHA256 hashed code verifier (default, recommended) diff --git a/docs/about/features.md b/docs/about/features.md index 3ee913db..33b32618 100644 --- a/docs/about/features.md +++ b/docs/about/features.md @@ -28,10 +28,9 @@ provides on overview of Headscale's feature and compatibility with the Tailscale routers](../ref/routes.md#automatically-approve-routes-of-a-subnet-router) and [exit nodes](../ref/routes.md#automatically-approve-an-exit-node-with-auto-approvers) - [x] [Tailscale SSH](https://tailscale.com/kb/1193/tailscale-ssh) -* [ ] Node registration using Single-Sign-On (OpenID Connect) ([GitHub label "OIDC"](https://github.com/juanfont/headscale/labels/OIDC)) +* [x] [Node registration using Single-Sign-On (OpenID Connect)](../ref/oidc.md) ([GitHub label "OIDC"](https://github.com/juanfont/headscale/labels/OIDC)) - [x] Basic registration - [x] Update user profile from identity provider - - [ ] Dynamic ACL support - [ ] OIDC groups cannot be used in ACLs - [ ] [Funnel](https://tailscale.com/kb/1223/funnel) ([#1040](https://github.com/juanfont/headscale/issues/1040)) - [ ] [Serve](https://tailscale.com/kb/1312/serve) ([#1234](https://github.com/juanfont/headscale/issues/1921)) diff --git a/docs/ref/oidc.md b/docs/ref/oidc.md index 871b20a2..e76082a1 100644 --- a/docs/ref/oidc.md +++ b/docs/ref/oidc.md @@ -1,66 +1,261 @@ -# Configuring headscale to use OIDC authentication +# OpenID Connect -In order to authenticate users through a centralized solution one must enable the OIDC integration. +Headscale supports authentication via external identity providers using OpenID Connect (OIDC). It features: -Known limitations: +* Autoconfiguration via OpenID Connect Discovery Protocol +* [Proof Key for Code Exchange (PKCE) code verification](#enable-pkce-recommended) +* Access control based on group membership +* Synchronization of [standard OIDC claims](#supported-oidc-claims) -- No dynamic ACL support -- OIDC groups cannot be used in ACLs +Please see [limitations](#limitations) for known issues and limitations. -## Basic configuration +## Configuration -In your `config.yaml`, customize this to your liking: +OpenID requires configuration in Headscale and in your identity provider: -```yaml title="config.yaml" -oidc: - # Block further startup until the OIDC provider is healthy and available - only_start_if_oidc_is_available: true - # Specified by your OIDC provider - issuer: "https://your-oidc.issuer.com/path" - # Specified/generated by your OIDC provider - client_id: "your-oidc-client-id" - client_secret: "your-oidc-client-secret" - # alternatively, set `client_secret_path` to read the secret from the file. - # It resolves environment variables, making integration to systemd's - # `LoadCredential` straightforward: - #client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret" - # as third option, it's also possible to load the oidc secret from environment variables - # set HEADSCALE_OIDC_CLIENT_SECRET to the required value +* 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. - # Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query - # parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email". - scope: ["openid", "profile", "email", "custom"] - # Optional: Passed on to the browser login request – used to tweak behaviour for the OIDC provider - extra_params: - domain_hint: example.com +### Basic configuration - # Optional: List allowed principal domains and/or users. If an authenticated user's domain is not in this list, - # the authentication request will be rejected. - allowed_domains: - - example.com - # Optional. Note that groups from Keycloak have a leading '/'. - allowed_groups: - - /headscale - # Optional. - allowed_users: - - alice@example.com +A basic configuration connects Headscale to an identity provider and typically requires: - # Optional: PKCE (Proof Key for Code Exchange) configuration - # 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: - # Enable or disable PKCE support (default: false) - enabled: false - # PKCE method to use: - # - plain: Use plain code verifier - # - S256: Use SHA256 hashed code verifier (default, recommended) - method: S256 +* 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, landing URL, 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: . PKCE is +recommended and needs to be configured for Headscale and the identity provider alike: + +=== "Headscale" + + ```yaml hl_lines="5-7" + oidc: + issuer: "https://sso.example.com" + client_id: "headscale" + client_secret: "generated-secret" + pkce: + enabled: true + method: S256 + ``` + +=== "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 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 + ``` + +### 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 claim | +| | `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 + +Any identity provider with OpenID Connect support should "just work" with Headscale, the following identity providers +are known to work: + +- [Keycloak](#keycloak) + +The section below contains provider specific notes and instructions. + +### Authelia +Authelia since v4.39.0, has removed most claims from the `ID Token`, they are still available when application queries [UserInfo Endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo). + +Following config restores sending 'default' claims in the `ID Token` + +For more information please read: [Authelia restore functionality prior to claims parameter](https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter) + + +```yaml +identity_providers: + oidc: + claims_policies: + default: + id_token: ['groups', 'email', 'email_verified', 'alt_emails', 'preferred_username', 'name'] + clients: + - client_id: 'headscale' + client_name: 'headscale' + client_secret: '' + public: false + claims_policy: 'default' + authorization_policy: 'two_factor' + require_pkce: true + pkce_challenge_method: 'S256' + redirect_uris: + - 'https://headscale.example.com/oidc/callback' + scopes: + - 'openid' + - 'profile' + - 'groups' + - 'email' + userinfo_signed_response_alg: 'none' + token_endpoint_auth_method: 'client_secret_basic' ``` -## Azure AD example +### Azure AD -In order to integrate headscale with Azure Active Directory, we'll need to provision an App Registration with the correct scopes and redirect URI. Here with Terraform: +In order to integrate Headscale with Azure Active Directory, we'll need to provision an App Registration with the correct scopes and redirect URI. Here with Terraform: ```hcl title="terraform.hcl" resource "azuread_application" "headscale" { @@ -148,15 +343,15 @@ oidc: prompt: select_account ``` -## Google OAuth Example +### Google OAuth -In order to integrate headscale with Google, you'll need to have a [Google Cloud Console](https://console.cloud.google.com) account. +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 +#### 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). @@ -178,36 +373,22 @@ However if you don't have a domain, or need to add users outside of your domain, You can also use `allowed_domains` and `allowed_users` to restrict the users who can authenticate. -## Authelia -Authelia since v4.39.0, has removed most claims from the `ID Token`, they are still available when application queries [UserInfo Endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo). +### Keycloak -Following config restores sending 'default' claims in the `ID Token` +Keycloak is fully supported by Headscale. -For more information please read: [Authelia restore functionality prior to claims parameter](https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter) +#### Additional configuration to use the allowed groups filter +Keycloak has no built-in client scope for the OIDC `groups` claim. The `groups` claim is used by Headscale to [authorize +access based on group membership](#authorize-users-with-filters). This extra configuration step is **only** needed if +you need to authorize access based on group membership. -```yaml -identity_providers: - oidc: - claims_policies: - default: - id_token: ['groups', 'email', 'email_verified', 'alt_emails', 'preferred_username', 'name'] - clients: - - client_id: 'headscale' - client_name: 'headscale' - client_secret: '' - public: false - claims_policy: 'default' - authorization_policy: 'two_factor' - require_pkce: true - pkce_challenge_method: 'S256' - redirect_uris: - - 'https://headscale.example.com/oidc/callback' - scopes: - - 'openid' - - 'profile' - - 'groups' - - 'email' - userinfo_signed_response_alg: 'none' - token_endpoint_auth_method: 'client_secret_basic' -``` +- 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 `/`. diff --git a/mkdocs.yml b/mkdocs.yml index 65cf4556..b096aed8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -176,7 +176,7 @@ nav: - Windows: usage/connect/windows.md - Reference: - Configuration: ref/configuration.md - - OIDC authentication: ref/oidc.md + - OpenID Connect: ref/oidc.md - Routes: ref/routes.md - TLS: ref/tls.md - ACLs: ref/acls.md