Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Support using groups from OIDC in ACLs #2366

Open
joachimtingvold opened this issue Jan 22, 2025 · 3 comments
Open

[Feature] Support using groups from OIDC in ACLs #2366

joachimtingvold opened this issue Jan 22, 2025 · 3 comments
Labels
enhancement New feature or request

Comments

@joachimtingvold
Copy link

joachimtingvold commented Jan 22, 2025

Use case

Fetch/use groups received via OIDC property/scope in ACLs. This way we don't have to manually maintain the group memberships in the ACL.

Description

This is a continuation from the discussion in #1121.

The feature does not need to implement full support for all OIDC providers (which would complicate things). This feature can simply be done by having a setting in headscale where you can specify the name of the OIDC property that contains the array of groups the user is a member of. If that is a non-empty list of groups, headscale can use that list of groups to evaluate what permissions the user will have. If a group received from OIDC is not present in headscale ACL config, it can simply be ignored.

Since headscale already have a field to define the OIDC scope in the configuration, no further changes on that aspect is needed. Simply adding an option like group_property: groups or similar in the oidc section of config.yaml would be sufficient from the configuration perspective.

Example config and JSON blobs using Authentik added below. Given these examples, the expectation would be that my user will have access to the host 10.1.1.1 (server1), even if there is no statically configured group "server1-admins" in acls.hujson.

OIDC Discover response:

{
  "issuer": "https://login.foo.bar/application/o/headscale/",
  "authorization_endpoint": "https://login.foo.bar/application/o/authorize/",
  "token_endpoint": "https://login.foo.bar/application/o/token/",
  "userinfo_endpoint": "https://login.foo.bar/application/o/userinfo/",
  "end_session_endpoint": "https://login.foo.bar/application/o/headscale/end-session/",
  "introspection_endpoint": "https://login.foo.bar/application/o/introspect/",
  "revocation_endpoint": "https://login.foo.bar/application/o/revoke/",
  "device_authorization_endpoint": "https://login.foo.bar/application/o/device/",
  "response_types_supported": [
    "code",
    "id_token",
    "id_token token",
    "code token",
    "code id_token",
    "code id_token token"
  ],
  "response_modes_supported": [
    "query",
    "fragment",
    "form_post"
  ],
  "jwks_uri": "https://login.foo.bar/application/o/headscale/jwks/",
  "grant_types_supported": [
    "authorization_code",
    "refresh_token",
    "implicit",
    "client_credentials",
    "password",
    "urn:ietf:params:oauth:grant-type:device_code"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "subject_types_supported": [
    "public"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_post",
    "client_secret_basic"
  ],
  "acr_values_supported": [
    "goauthentik.io/providers/oauth2/default"
  ],
  "scopes_supported": [
    "profile",
    "openid",
    "email"
  ],
  "request_parameter_supported": false,
  "claims_supported": [
    "sub",
    "iss",
    "aud",
    "exp",
    "iat",
    "auth_time",
    "acr",
    "amr",
    "nonce",
    "email",
    "email_verified",
    "name",
    "given_name",
    "family_name",
    "preferred_username",
    "nickname",
    "groups"
  ],
  "claims_parameter_supported": false,
  "code_challenge_methods_supported": [
    "plain",
    "S256"
  ]
}

OIDC userinfo response:

{
  "sub": "65cbb96f55633865c19c6cccb8b2bb9e575dc7d7b53d3e9bbcaf4fa7bfcda8a8",
  "email": "[email protected]",
  "email_verified": true,
  "name": "Joachim Tingvold",
  "given_name": "Joachim",
  "family_name": "Tingvold",
  "preferred_username": "jocke",
  "nickname": "jocke",
  "groups": [
    "vpn-admins",
    "server-admins",
    "server1-admins",
    "server2-admins",
    "bms-system-admins"
  ],
  "nonce": "1a07de44633382b528b6f2f114b9ff10"
}

OIDC JWT payload:

{
  "aud": "96crVkZhfTueYrzoP9jup8WPDqbGrrtFkmrbGeN3",
  "name": "Joachim Tingvold",
  "email": "[email protected]",
  "sid": "7cc65765730adc54a736e4e774bc73f4f44d37d02815dcdd3cc9220654c79823",
  "auth_time": 1737550394,
  "acr": "goauthentik.io/providers/oauth2/default",
  "amr": [
    "pwd",
    "mfa"
  ],
  "email_verified": true,
  "given_name": "Joachim",
  "family_name": "Tingvold",
  "preferred_username": "jocke",
  "nickname": "jocke",
  "azp": "96crVkZhfTueYrzoP9jup8WPDqbGrrtFkmrbGeN3",
  "sub": "65cbb96f55633865c19c6cccb8b2bb9e575dc7d7b53d3e9bbcaf4fa7bfcda8a8",
  "iat": 1737550500,
  "uid": "PM9TnbyTc7BJmvZPQm6tz9HCnLLMRqAZNUWZG8WR",
  "groups": [
    "vpn-admins",
    "server-admins",
    "server1-admins",
    "server2-admins",
    "bms-system-admins"
  ],
  "nonce": "1a07de44633382b528b6f2f114b9ff10",
  "iss": "https://login.keklolwtf.no/application/o/headscale/",
  "exp": 1737550800
}

oidc-section of config.yaml:

oidc:
  only_start_if_oidc_is_available: true
  issuer: "https://login.foo.bar/application/o/headscale/"
  client_id: "some client id"
  client_secret: "super secret stuff"
  expiry: 7d
  use_expiry_from_token: false
  scope: ["openid", "profile", "email"]
  strip_email_domain: false
  group_property: groups

Relevant parts of acls.hujson:

{
  "groups": {
  },
  "tagOwners": {
  },
  "hosts": {
    "server1": "10.1.1.1",
  },
  "acls": [
    {
      "action": "accept",
      "src": ["group:server1-admins"],
      "dst": [
        "server1:*",
      ]
    }
  ],
}

@joachimtingvold joachimtingvold added the enhancement New feature or request label Jan 22, 2025
@joachimtingvold joachimtingvold changed the title [Feature] Add support for using groups from OIDC in ACLs [Feature] Support using groups from OIDC in ACLs Jan 22, 2025
@Nathanael-Mtd
Copy link

Useful feature for many of us !

About the implementation idea, it's more a user-group import and store to ACL file which be needed, when OIDC (re-)login to headscale is done (first time login or on expiration).
Because that's only during this step OIDC user info is handled by Headscale, not at every Tailscale client startup.

Like that we can dynamically fill the ACL policy with user mappings to groups, and be handle manual user groups changes when expiration is too far.
But maybe it can have an impact on Headscale if there are many ACL reloads.

Overall there are some limitations with OpenID provisioning because we can't handle automatic groups changes and account lock/deletion before node expiration.

@joachimtingvold
Copy link
Author

About the implementation idea, it's more a user-group import and store to ACL file which be needed, when OIDC (re-)login to headscale is done (first time login or on expiration). Because that's only during this step OIDC user info is handled by Headscale, not at every Tailscale client startup.

Fair enough. Would we need to really write/update acls.hujson for every login? How does Headscale handle node expirations during a reload/restart of the headscale service itself? Does node expire due to that (i.e. trigger a relogin), or would it actually maintain already cached nodes? If they expire, the group(s) could then technically also be held in memory? (rather than writing/altering acls.hujson directly). If nodes are not expired due to a service restart, then you would of course need to do so.

Like that we can dynamically fill the ACL policy with user mappings to groups, and be handle manual user groups changes when expiration is too far. But maybe it can have an impact on Headscale if there are many ACL reloads.

There might be a potential scaling issue in play here, depending on how many users there are on a given headscale instance. I would wager that this would not be the case for the majority of the headscale instances out there (in which case it would probably make more sense for them to have scripts that would populate acls.hujson groups at regular intervals that fetches group membership from whatever IdP they're using).

Overall there are some limitations with OpenID provisioning because we can't handle automatic groups changes and account lock/deletion before node expiration.

That would also be fine for most usecases for this, I would say.

@kradalby
Copy link
Collaborator

It sounds like @Nathanael-Mtd is essentially describing SCIM v2.

I think that would be an unrealistic undertaking, and out of scope.

A reasonable compromise sounds like we are able to sync in groups, and they can be used. The admin can then decide the "tolerance" they how for groups being out of sync by setting a short or long expiry time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants