Files
headscale/development/ref/oidc/index.html

42 lines
61 KiB
HTML
Raw Permalink Normal View History

<!doctype html><html lang=en class=no-js> <head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=description content="An open source, self-hosted implementation of the Tailscale control server."><meta name=author content="Headscale authors"><link href=https://juanfont.github.io/headscale/development/ref/oidc/ rel=canonical><link href=../configuration/ rel=prev><link href=../routes/ rel=next><link rel=icon href=../../assets/favicon.png><meta name=generator content="mkdocs-1.6.1, mkdocs-material-9.6.16"><title>OpenID Connect - Headscale</title><link rel=stylesheet href=../../assets/stylesheets/main.7e37652d.min.css><link rel=stylesheet href=../../assets/stylesheets/palette.06af60db.min.css><link rel=preconnect href=https://fonts.gstatic.com crossorigin><link rel=stylesheet href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback"><style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style><script>__md_scope=new URL("../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script><meta property=og:type content=website><meta property=og:title content="OpenID Connect - Headscale"><meta property=og:description content="An open source, self-hosted implementation of the Tailscale control server."><meta property=og:image content=https://juanfont.github.io/headscale/development/assets/images/social/ref/oidc.png><meta property=og:image:type content=image/png><meta property=og:image:width content=1200><meta property=og:image:height content=630><meta content=https://juanfont.github.io/headscale/development/ref/oidc/ property=og:url><meta name=twitter:card content=summary_large_image><meta name=twitter:title content="OpenID Connect - Headscale"><meta name=twitter:description content="An open source, self-hosted implementation of the Tailscale control server."><meta name=twitter:image content=https://juanfont.github.io/headscale/development/assets/images/social/ref/oidc.png></head> <body dir=ltr data-md-color-scheme=default data-md-color-primary=white data-md-color-accent=indigo> <input class=md-toggle data-md-toggle=drawer type=checkbox id=__drawer autocomplete=off> <input class=md-toggle data-md-toggle=search type=checkbox id=__search autocomplete=off> <label class=md-overlay for=__drawer></label> <div data-md-component=skip> <a href=#openid-connect class=md-skip> Skip to content </a> </div> <div data-md-component=announce> </div> <div data-md-color-scheme=default data-md-component=outdated hidden> </div> <header class=md-header data-md-component=header> <nav class="md-header__inner md-grid" aria-label=Header> <a href=../.. title=Headscale class="md-header__button md-logo" aria-label=Headscale data-md-component=logo> <img src=../../logo/headscale3-dots.svg alt=logo> </a> <label class="md-header__button md-icon" for=__drawer> <svg xmlns=http://www.w3.org/2000/svg viewbox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg> </label> <div class=md-header__title data-md-component=header-title> <div class=md-header__ellipsis> <div class=md-header__topic> <span class=md-ellipsis> Headscale </span> </div> <div class=md-header__topic data-md-component=header-topic> <span class=md-ellipsis> OpenID Connect </span> </div> </div> </div> <form class=md-header__option data-md-component=palette> <input class=md-option data-md-color-media data-md-color-scheme=default data-md-color-primary=white data-md-color-accent=indigo aria-label="Switch to dark mode" type=radio name=__palette id=__palette_0> <label class="md-header__button md-icon" title="Switch to dark mode" for=__palette_1 hidden> <svg xmlns=http://www.w3.org/2000/svg viewbox="0 0 24 24"><path d="M12 8a4 4 0 0 0-4 4 4 4 0 0 0 4 4 4 4 0 0 0 4-4 4 4 0 0 0-4-4m0 10a6 6 0 0 1-6-6 6 6 0 0 1 6-6 6 6 0 0 1 6 6 6 6 0 0 1-6 6m8-9.31V4h-4.69L12 .69
</span><span id=__span-0-2><a id=__codelineno-0-2 name=__codelineno-0-2 href=#__codelineno-0-2></a><span class=w> </span><span class=nt>issuer</span><span class=p>:</span><span class=w> </span><span class=s>&quot;https://sso.example.com&quot;</span>
</span><span id=__span-0-3><a id=__codelineno-0-3 name=__codelineno-0-3 href=#__codelineno-0-3></a><span class=w> </span><span class=nt>client_id</span><span class=p>:</span><span class=w> </span><span class=s>&quot;headscale&quot;</span>
</span><span id=__span-0-4><a id=__codelineno-0-4 name=__codelineno-0-4 href=#__codelineno-0-4></a><span class=w> </span><span class=nt>client_secret</span><span class=p>:</span><span class=w> </span><span class=s>&quot;generated-secret&quot;</span>
</span></code></pre></div> </div> <div class=tabbed-block> <ul> <li>Create a new confidential client (<code>Client ID</code>, <code>Client secret</code>)</li> <li>Add Headscale's OIDC callback URL as valid redirect URL: <code>https://headscale.example.com/oidc/callback</code></li> <li>Configure additional parameters to improve user experience such as: name, description, logo, …</li> </ul> </div> </div> </div> <h3 id=enable-pkce-recommended>Enable PKCE (recommended)<a class=headerlink href=#enable-pkce-recommended title="Permanent link">&para;</a></h3> <p>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: <a href=https://datatracker.ietf.org/doc/html/rfc7636>https://datatracker.ietf.org/doc/html/rfc7636</a>. PKCE is recommended and needs to be configured for Headscale and the identity provider alike:</p> <div class="tabbed-set tabbed-alternate" data-tabs=2:2><input checked=checked id=__tabbed_2_1 name=__tabbed_2 type=radio><input id=__tabbed_2_2 name=__tabbed_2 type=radio><div class=tabbed-labels><label for=__tabbed_2_1>Headscale</label><label for=__tabbed_2_2>Identity provider</label></div> <div class=tabbed-content> <div class=tabbed-block> <div class="language-yaml highlight"><pre><span></span><code><span id=__span-1-1><a id=__codelineno-1-1 name=__codelineno-1-1 href=#__codelineno-1-1></a><span class=nt>oidc</span><span class=p>:</span>
</span><span id=__span-1-2><a id=__codelineno-1-2 name=__codelineno-1-2 href=#__codelineno-1-2></a><span class=w> </span><span class=nt>issuer</span><span class=p>:</span><span class=w> </span><span class=s>&quot;https://sso.example.com&quot;</span>
</span><span id=__span-1-3><a id=__codelineno-1-3 name=__codelineno-1-3 href=#__codelineno-1-3></a><span class=w> </span><span class=nt>client_id</span><span class=p>:</span><span class=w> </span><span class=s>&quot;headscale&quot;</span>
</span><span id=__span-1-4><a id=__codelineno-1-4 name=__codelineno-1-4 href=#__codelineno-1-4></a><span class=w> </span><span class=nt>client_secret</span><span class=p>:</span><span class=w> </span><span class=s>&quot;generated-secret&quot;</span>
</span><span id=__span-1-5><a id=__codelineno-1-5 name=__codelineno-1-5 href=#__codelineno-1-5></a><span class=hll><span class=w> </span><span class=nt>pkce</span><span class=p>:</span>
</span></span><span id=__span-1-6><a id=__codelineno-1-6 name=__codelineno-1-6 href=#__codelineno-1-6></a><span class=hll><span class=w> </span><span class=nt>enabled</span><span class=p>:</span><span class=w> </span><span class="l l-Scalar l-Scalar-Plain">true</span>
</span></span></code></pre></div> </div> <div class=tabbed-block> <ul> <li>Enable PKCE for the headscale client</li> <li>Set the PKCE challenge method to "S256"</li> </ul> </div> </div> </div> <h3 id=authorize-users-with-filters>Authorize users with filters<a class=headerlink href=#authorize-users-with-filters title="Permanent link">&para;</a></h3> <p>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.</p> <div class="tabbed-set tabbed-alternate" data-tabs=3:3><input checked=checked id=__tabbed_3_1 name=__tabbed_3 type=radio><input id=__tabbed_3_2 name=__tabbed_3 type=radio><input id=__tabbed_3_3 name=__tabbed_3 type=radio><div class=tabbed-labels><label for=__tabbed_3_1>Allowed domains</label><label for=__tabbed_3_2>Allowed users/emails</label><label for=__tabbed_3_3>Allowed groups</label></div> <div class=tabbed-content> <div class=tabbed-block> <ul> <li>Check the email domain of each authenticating user against the list of allowed domains and only authorize users whose email domain matches <code>example.com</code>.</li> <li>Access allowed: <code>alice@example.com</code></li> <li>Access denied: <code>bob@example.net</code></li> </ul> <div class="language-yaml highlight"><pre><span></span><code><span id=__span-2-1><a id=__codelineno-2-1 name=__codelineno-2-1 href=#__codelineno-2-1></a><span class=nt>oidc</span><span class=p>:</span>
</span><span id=__span-2-2><a id=__codelineno-2-2 name=__codelineno-2-2 href=#__codelineno-2-2></a><span class=w> </span><span class=nt>issuer</span><span class=p>:</span><span class=w> </span><span class=s>&quot;https://sso.example.com&quot;</span>
</span><span id=__span-2-3><a id=__codelineno-2-3 name=__codelineno-2-3 href=#__codelineno-2-3></a><span class=w> </span><span class=nt>client_id</span><span class=p>:</span><span class=w> </span><span class=s>&quot;headscale&quot;</span>
</span><span id=__span-2-4><a id=__codelineno-2-4 name=__codelineno-2-4 href=#__codelineno-2-4></a><span class=w> </span><span class=nt>client_secret</span><span class=p>:</span><span class=w> </span><span class=s>&quot;generated-secret&quot;</span>
</span><span id=__span-2-5><a id=__codelineno-2-5 name=__codelineno-2-5 href=#__codelineno-2-5></a><span class=hll><span class=w> </span><span class=nt>allowed_domains</span><span class=p>:</span>
</span></span><span id=__span-2-6><a id=__codelineno-2-6 name=__codelineno-2-6 href=#__codelineno-2-6></a><span class=hll><span class=w> </span><span class="p p-Indicator">-</span><span class=w> </span><span class=s>&quot;example.com&quot;</span>
</span></span></code></pre></div> </div> <div class=tabbed-block> <ul> <li>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 <code>allowed_users</code> list.</li> <li>Access allowed: <code>alice@example.com</code>, <code>bob@example.net</code></li> <li>Access denied: <code>mallory@example.net</code></li> </ul> <div class="language-yaml highlight"><pre><span></span><code><span id=__span-3-1><a id=__codelineno-3-1 name=__codelineno-3-1 href=#__codelineno-3-1></a><span class=nt>oidc</span><span class=p>:</span>
</span><span id=__span-3-2><a id=__codelineno-3-2 name=__codelineno-3-2 href=#__codelineno-3-2></a><span class=w> </span><span class=nt>issuer</span><span class=p>:</span><span class=w> </span><span class=s>&quot;https://sso.example.com&quot;</span>
</span><span id=__span-3-3><a id=__codelineno-3-3 name=__codelineno-3-3 href=#__codelineno-3-3></a><span class=w> </span><span class=nt>client_id</span><span class=p>:</span><span class=w> </span><span class=s>&quot;headscale&quot;</span>
</span><span id=__span-3-4><a id=__codelineno-3-4 name=__codelineno-3-4 href=#__codelineno-3-4></a><span class=w> </span><span class=nt>client_secret</span><span class=p>:</span><span class=w> </span><span class=s>&quot;generated-secret&quot;</span>
</span><span id=__span-3-5><a id=__codelineno-3-5 name=__codelineno-3-5 href=#__codelineno-3-5></a><span class=hll><span class=w> </span><span class=nt>allowed_users</span><span class=p>:</span>
</span></span><span id=__span-3-6><a id=__codelineno-3-6 name=__codelineno-3-6 href=#__codelineno-3-6></a><span class=hll><span class=w> </span><span class="p p-Indicator">-</span><span class=w> </span><span class=s>&quot;alice@example.com&quot;</span>
</span></span><span id=__span-3-7><a id=__codelineno-3-7 name=__codelineno-3-7 href=#__codelineno-3-7></a><span class=hll><span class=w> </span><span class="p p-Indicator">-</span><span class=w> </span><span class=s>&quot;bob@example.net&quot;</span>
</span></span></code></pre></div> </div> <div class=tabbed-block> <ul> <li>Use the OIDC <code>groups</code> 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.</li> <li>Access allowed: users in the <code>headscale_users</code> group</li> <li>Access denied: users without groups, users with other groups</li> </ul> <div class="language-yaml highlight"><pre><span></span><code><span id=__span-4-1><a id=__codelineno-4-1 name=__codelineno-4-1 href=#__codelineno-4-1></a><span class=nt>oidc</span><span class=p>:</span>
</span><span id=__span-4-2><a id=__codelineno-4-2 name=__codelineno-4-2 href=#__codelineno-4-2></a><span class=w> </span><span class=nt>issuer</span><span class=p>:</span><span class=w> </span><span class=s>&quot;https://sso.example.com&quot;</span>
</span><span id=__span-4-3><a id=__codelineno-4-3 name=__codelineno-4-3 href=#__codelineno-4-3></a><span class=w> </span><span class=nt>client_id</span><span class=p>:</span><span class=w> </span><span class=s>&quot;headscale&quot;</span>
</span><span id=__span-4-4><a id=__codelineno-4-4 name=__codelineno-4-4 href=#__codelineno-4-4></a><span class=w> </span><span class=nt>client_secret</span><span class=p>:</span><span class=w> </span><span class=s>&quot;generated-secret&quot;</span>
</span><span id=__span-4-5><a id=__codelineno-4-5 name=__codelineno-4-5 href=#__codelineno-4-5></a><span class=hll><span class=w> </span><span class=nt>scope</span><span class=p>:</span><span class=w> </span><span class="p p-Indicator">[</span><span class=s>&quot;openid&quot;</span><span class="p p-Indicator">,</span><span class=w> </span><span class=s>&quot;profile&quot;</span><span class="p p-Indicator">,</span><span class=w> </span><span class=s>&quot;email&quot;</span><span class="p p-Indicator">,</span><span class=w> </span><span class=s>&quot;groups&quot;</span><span class="p p-Indicator">]</span>
</span></span><span id=__span-4-6><a id=__codelineno-4-6 name=__codelineno-4-6 href=#__codelineno-4-6></a><span class=hll><span class=w> </span><span class=nt>allowed_groups</span><span class=p>:</span>
</span></span><span id=__span-4-7><a id=__codelineno-4-7 name=__codelineno-4-7 href=#__codelineno-4-7></a><span class=hll><span class=w> </span><span class="p p-Indicator">-</span><span class=w> </span><span class=s>&quot;headscale_users&quot;</span>
</span></span></code></pre></div> </div> </div> </div> <h3 id=customize-node-expiration>Customize node expiration<a class=headerlink href=#customize-node-expiration title="Permanent link">&para;</a></h3> <p>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.</p> <div class="tabbed-set tabbed-alternate" data-tabs=4:2><input checked=checked id=__tabbed_4_1 name=__tabbed_4 type=radio><input id=__tabbed_4_2 name=__tabbed_4 type=radio><div class=tabbed-labels><label for=__tabbed_4_1>Customize node expiration</label><label for=__tabbed_4_2>Use expiration from Access Token</label></div> <div class=tabbed-content> <div class=tabbed-block> <div class="language-yaml highlight"><pre><span></span><code><span id=__span-5-1><a id=__codelineno-5-1 name=__codelineno-5-1 href=#__codelineno-5-1></a><span class=nt>oidc</span><span class=p>:</span>
</span><span id=__span-5-2><a id=__codelineno-5-2 name=__codelineno-5-2 href=#__codelineno-5-2></a><span class=w> </span><span class=nt>issuer</span><span class=p>:</span><span class=w> </span><span class=s>&quot;https://sso.example.com&quot;</span>
</span><span id=__span-5-3><a id=__codelineno-5-3 name=__codelineno-5-3 href=#__codelineno-5-3></a><span class=w> </span><span class=nt>client_id</span><span class=p>:</span><span class=w> </span><span class=s>&quot;headscale&quot;</span>
</span><span id=__span-5-4><a id=__codelineno-5-4 name=__codelineno-5-4 href=#__codelineno-5-4></a><span class=w> </span><span class=nt>client_secret</span><span class=p>:</span><span class=w> </span><span class=s>&quot;generated-secret&quot;</span>
</span><span id=__span-5-5><a id=__codelineno-5-5 name=__codelineno-5-5 href=#__codelineno-5-5></a><span class=hll><span class=w> </span><span class=nt>expiry</span><span class=p>:</span><span class=w> </span><span class="l l-Scalar l-Scalar-Plain">30d</span><span class=w> </span><span class=c1># Use 0 to disable node expiration</span>
</span></span></code></pre></div> </div> <div class=tabbed-block> <p>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.</p> <div class="language-yaml highlight"><pre><span></span><code><span id=__span-6-1><a id=__codelineno-6-1 name=__codelineno-6-1 href=#__codelineno-6-1></a><span class=nt>oidc</span><span class=p>:</span>
</span><span id=__span-6-2><a id=__codelineno-6-2 name=__codelineno-6-2 href=#__codelineno-6-2></a><span class=w> </span><span class=nt>issuer</span><span class=p>:</span><span class=w> </span><span class=s>&quot;https://sso.example.com&quot;</span>
</span><span id=__span-6-3><a id=__codelineno-6-3 name=__codelineno-6-3 href=#__codelineno-6-3></a><span class=w> </span><span class=nt>client_id</span><span class=p>:</span><span class=w> </span><span class=s>&quot;headscale&quot;</span>
</span><span id=__span-6-4><a id=__codelineno-6-4 name=__codelineno-6-4 href=#__codelineno-6-4></a><span class=w> </span><span class=nt>client_secret</span><span class=p>:</span><span class=w> </span><span class=s>&quot;generated-secret&quot;</span>
</span><span id=__span-6-5><a id=__codelineno-6-5 name=__codelineno-6-5 href=#__codelineno-6-5></a><span class=hll><span class=w> </span><span class=nt>use_expiry_from_token</span><span class=p>:</span><span class=w> </span><span class="l l-Scalar l-Scalar-Plain">true</span>
</span></span></code></pre></div> </div> </div> </div> <div class="admonition tip"> <p class=admonition-title>Expire a node and force re-authentication</p> <p>A node can be expired immediately via: <div class="language-console highlight"><pre><span></span><code><span id=__span-7-1><a id=__codelineno-7-1 name=__codelineno-7-1 href=#__codelineno-7-1></a><span class=go>headscale node expire -i &lt;NODE_ID&gt;</span>
</span></code></pre></div></p> </div> <h3 id=reference-a-user-in-the-policy>Reference a user in the policy<a class=headerlink href=#reference-a-user-in-the-policy title="Permanent link">&para;</a></h3> <p>You may refer to users in the Headscale policy via:</p> <ul> <li>Email address</li> <li>Username</li> <li>Provider identifier (only available in the database or from your identity provider)</li> </ul> <div class="admonition note"> <p class=admonition-title>A user identifier in the policy must contain a single <code>@</code></p> <p>The Headscale policy requires a single <code>@</code> to reference a user. If the username or provider identifier doesn't already contain a single <code>@</code>, it needs to be appended at the end. For example: the username <code>ssmith</code> has to be written as <code>ssmith@</code> to be correctly identified as user within the policy.</p> </div> <div class="admonition warning"> <p class=admonition-title>Email address or username might be updated by users</p> <p>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.</p> </div> <h2 id=supported-oidc-claims>Supported OIDC claims<a class=headerlink href=#supported-oidc-claims title="Permanent link">&para;</a></h2> <p>Headscale uses <a href=https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims>the standard OIDC claims</a> to populate and update its local user profile on each login. OIDC claims are read from the ID Token or from the UserInfo endpoint.</p> <table> <thead> <tr> <th>Headscale profile</th> <th>OIDC claim</th> <th>Notes / examples</th> </tr> </thead> <tbody> <tr> <td>email address</td> <td><code>email</code></td> <td>Only used when <code>email_verified: true</code></td> </tr> <tr> <td>display name</td> <td><code>name</code></td> <td>eg: <code>Sam Smith</code></td> </tr> <tr> <td>username</td> <td><code>preferred_username</code></td> <td>Depends on identity provider, eg: <code>ssmith</code>, <code>ssmith@idp.example.com</code>, <code>\\example.com\ssmith</code></td> </tr> <tr> <td>profile picture</td> <td><code>picture</code></td> <td>URL to a profile picture or avatar</td> </tr> <tr> <td>provider identifier</td> <td><code>iss</code>, <code>sub</code></td> <td>A stable and unique identifier for a user, typically a combination of <code>iss</code> and <code>sub</code> OIDC claims</td> </tr> <tr> <td></td> <td><code>groups</code></td> <td><a href=#authorize-users-with-filters>Only used to filter for allowed groups</a></td> </tr> </tbody> </table> <h2 id=limitations>Limitations<a class=headerlink href=#limitations title="Permanent link">&para;</a></h2> <ul> <li>Support for OpenID Connect aims to be generic and vendor independent. It offers only limited support for quirks of specific identity providers.</li> <li>OIDC groups cannot be used in ACLs.</li> <li>The username provided by the identity provider needs to adhere to this pattern:<ul> <li>The username must be at least two characters long.</li> <li>It must only contain letters, digits, hyphens, dots, underscores, and up to a single <code>@</code>.</li> <li>The username must start with a letter.</li> </ul> </li> <li>A user's email address is only synchronized to the local user profile when the identity provider marks the email address as verified (<code>email_verified: true</code>).</li> </ul> <p>Please see the <a href=https://github.com/juanfont/headscale/labels/OIDC>GitHub label "OIDC"</a> for OIDC related issues.</p> <h2 id=identity-provider-specific-configuration>Identity provider specific configuration<a class=headerlink href=#identity-provider-specific-configuration title="Permanent link">&para;</a></h2> <div class="admonition warning"> <p class=admonition-title>Third-party software and services</p> <p>This section of the documentation is specific for third-part