diff --git a/docs/README.md b/docs/README.md index f215e80f..fcf819bc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,7 @@ please ask on [Discord](https://discord.gg/c84AZQhmpx) instead of opening an Iss - [Running headscale on Linux](running-headscale-linux.md) - [Control headscale remotely](remote-cli.md) - [Using a Windows client with headscale](windows-client.md) +- [Configuring OIDC](oidc.md) ### References diff --git a/docs/oidc.md b/docs/oidc.md new file mode 100644 index 00000000..16773817 --- /dev/null +++ b/docs/oidc.md @@ -0,0 +1,137 @@ +# Configuring Headscale to use OIDC authentication + +In order to authenticate users through a centralized solution one must enable the OIDC integration. + +Known limitations: + +- No dynamic ACL support +- OIDC groups cannot be used in ACLs + +## Basic configuration + +In your `config.yaml`, customize this to your liking: + +```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" + + # 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 + + # 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 + + # If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed. + # This will transform `first-name.last-name@example.com` to the namespace `first-name.last-name` + # If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following + # namespace: `first-name.last-name.example.com` + strip_email_domain: true +``` + +## Azure AD example + +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 +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 +oidc: + issuer: "https://login.microsoftonline.com//v2.0" + client_id: "" + client_secret: "" + + # 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 +```