mirror of
https://github.com/juanfont/headscale.git
synced 2025-07-18 13:46:45 +02:00
Restructure and rewrite the OpenID Connect documentation. Start from the most minimal configuration and describe what needs to be done both in Headscale and the identity provider. Describe additional features such as PKCE and authorization filters in a generic manner with examples. Document how Headscale populates its user profile and how it relates to OIDC claims. This is a revised version from the table in the changelog. Document the validation rules for fields and extend known limitations. Sort the provider specific section alphabetically and add a section for Keycloak with a brief description on how to configure the OIDC groups claim. Update the description for the oidc section in the example configuration. Give a short explanation of each configuration setting. All documentend features were tested with Headscale 0.26 (using a fresh database each time) using the following identity providers: * Keycloak
395 lines
15 KiB
Markdown
395 lines
15 KiB
Markdown
# 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)
|
|
* Access control based on group membership
|
|
* Synchronization of [standard OIDC claims](#supported-oidc-claims)
|
|
|
|
Please see [limitations](#limitations) for known issues and limitations.
|
|
|
|
## Configuration
|
|
|
|
OpenID requires configuration in Headscale and in 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, 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: <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-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 <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 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
|
|
|
|
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" {
|
|
display_name = "Headscale"
|
|
|
|
sign_in_audience = "AzureADMyOrg"
|
|
fallback_public_client_enabled = false
|
|
|
|
required_resource_access {
|
|
// Microsoft Graph
|
|
resource_app_id = "00000003-0000-0000-c000-000000000000"
|
|
|
|
resource_access {
|
|
// scope: profile
|
|
id = "14dad69e-099b-42c9-810b-d002981feec1"
|
|
type = "Scope"
|
|
}
|
|
resource_access {
|
|
// scope: openid
|
|
id = "37f7f235-527c-4136-accd-4a02d197296e"
|
|
type = "Scope"
|
|
}
|
|
resource_access {
|
|
// scope: email
|
|
id = "64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0"
|
|
type = "Scope"
|
|
}
|
|
}
|
|
web {
|
|
# Points at your running headscale instance
|
|
redirect_uris = ["https://headscale.example.com/oidc/callback"]
|
|
|
|
implicit_grant {
|
|
access_token_issuance_enabled = false
|
|
id_token_issuance_enabled = true
|
|
}
|
|
}
|
|
|
|
group_membership_claims = ["SecurityGroup"]
|
|
optional_claims {
|
|
# Expose group memberships
|
|
id_token {
|
|
name = "groups"
|
|
}
|
|
}
|
|
}
|
|
|
|
resource "azuread_application_password" "headscale-application-secret" {
|
|
display_name = "Headscale Server"
|
|
application_object_id = azuread_application.headscale.object_id
|
|
}
|
|
|
|
resource "azuread_service_principal" "headscale" {
|
|
application_id = azuread_application.headscale.application_id
|
|
}
|
|
|
|
resource "azuread_service_principal_password" "headscale" {
|
|
service_principal_id = azuread_service_principal.headscale.id
|
|
end_date_relative = "44640h"
|
|
}
|
|
|
|
output "headscale_client_id" {
|
|
value = azuread_application.headscale.application_id
|
|
}
|
|
|
|
output "headscale_client_secret" {
|
|
value = azuread_application_password.headscale-application-secret.value
|
|
}
|
|
```
|
|
|
|
And in your headscale `config.yaml`:
|
|
|
|
```yaml title="config.yaml"
|
|
oidc:
|
|
issuer: "https://login.microsoftonline.com/<tenant-UUID>/v2.0"
|
|
client_id: "<client-id-from-terraform>"
|
|
client_secret: "<client-secret-from-terraform>"
|
|
|
|
# Optional: add "groups"
|
|
scope: ["openid", "profile", "email"]
|
|
extra_params:
|
|
# Use your own domain, associated with Azure AD
|
|
domain_hint: example.com
|
|
# Optional: Force the Azure AD account picker
|
|
prompt: select_account
|
|
```
|
|
|
|
### Google OAuth
|
|
|
|
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`, use `https://example.com/oidc/callback`, replacing example.com with your headscale URL.
|
|
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. Edit your headscale config, under `oidc`, filling in your `client_id` and `client_secret`:
|
|
```yaml title="config.yaml"
|
|
oidc:
|
|
issuer: "https://accounts.google.com"
|
|
client_id: ""
|
|
client_secret: ""
|
|
scope: ["openid", "profile", "email"]
|
|
```
|
|
|
|
You can also use `allowed_domains` and `allowed_users` to restrict the users who can authenticate.
|
|
|
|
### 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. 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.
|
|
|
|
- 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 `/`.
|