From 7a0ff079c780b4e25200d32edd002b097a4f9737 Mon Sep 17 00:00:00 2001 From: Jack Carter <128555021+SunsetDrifter@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:26:18 +0200 Subject: [PATCH 1/7] docs: add ADFS with Web Application Proxy self-hosted guide New guide for integrating on-prem Active Directory with ADFS as an OIDC identity provider for self-hosted NetBird. Covers ADFS on a dedicated member server, Web Application Proxy in a DMZ, Duo ADFS MFA Adapter, claim transform rules, and the required NetBird configuration (NETBIRD_TOKEN_SOURCE=idToken, NETBIRD_AUTH_USER_ID_CLAIM=upn). --- src/components/NavigationDocs.jsx | 4 + .../selfhosted/identity-providers/adfs.mdx | 710 ++++++++++++++++++ 2 files changed, 714 insertions(+) create mode 100644 src/pages/selfhosted/identity-providers/adfs.mdx diff --git a/src/components/NavigationDocs.jsx b/src/components/NavigationDocs.jsx index d32c6945..da2ece55 100644 --- a/src/components/NavigationDocs.jsx +++ b/src/components/NavigationDocs.jsx @@ -604,6 +604,10 @@ export const docsNavigation = [ title: 'PocketID', href: '/selfhosted/identity-providers/pocketid', }, + { + title: 'ADFS', + href: '/selfhosted/identity-providers/adfs', + }, ], }, { diff --git a/src/pages/selfhosted/identity-providers/adfs.mdx b/src/pages/selfhosted/identity-providers/adfs.mdx new file mode 100644 index 00000000..4b866ad3 --- /dev/null +++ b/src/pages/selfhosted/identity-providers/adfs.mdx @@ -0,0 +1,710 @@ +import {Note} from "@/components/mdx"; + +# ADFS with NetBird Self-Hosted + +[Active Directory Federation Services (ADFS)](https://learn.microsoft.com/en-us/windows-server/identity/active-directory-federation-services) is Microsoft's on-premises identity federation service. It lets organizations use their existing Active Directory accounts for single sign-on to external applications via OpenID Connect, OAuth 2.0, and SAML 2.0. ADFS is a common choice when on-prem AD is already the source of truth for user identity and the organization does not want to move authentication to a cloud IdP. + +This guide walks through deploying ADFS as an OIDC identity provider for a self-hosted NetBird instance, following Microsoft's recommended topology (dedicated ADFS member server fronted by a Web Application Proxy in a DMZ) with Cisco Duo MFA enforced at authentication. + +## Overview + +**What this guide covers:** + +- Deploying ADFS on a dedicated member server (separate from the domain controller) +- Deploying Web Application Proxy (WAP) in a DMZ to front ADFS for external access +- Configuring an ADFS OIDC application for NetBird with the correct claim rules +- Integrating the Duo ADFS MFA Adapter +- Firewall rules between the zones +- Connecting ADFS to NetBird as a generic OIDC provider + +**What this guide does not cover:** + +- Active Directory Domain Services installation +- NetBird self-hosted deployment — see the [self-hosted guide](/selfhosted/selfhosted-guide) +- Duo Authentication Proxy setup (this guide uses the Duo ADFS Adapter, which is a separate product) + +--- + +## Architecture + +``` + INTERNET + | + [ Firewall ] + | + +---------+---------+ + | DMZ | + | | + | Web Application | + | Proxy (WAP) | + | TCP 443 inbound | + | | + | NetBird Mgmt | + | TCP 443, UDP 3478| + | inbound | + +---------+---------+ + | + [ Firewall ] + | TCP 443 only + | (WAP to ADFS) + +---------+---------+ + | Corporate LAN | + | | + | ADFS Server | + | (member server) | + | | + | Domain | + | Controller | + +-------------------+ +``` + +This topology follows Microsoft's recommended deployment for ADFS: + +- The **Domain Controller** stays on the internal corporate network. It has no path to the internet, direct or indirect. +- The **ADFS Server** runs on a separate, domain-joined member server on the corporate network. Token signing keys never leave this server, and external devices never connect to it directly. +- The **Web Application Proxy (WAP)** sits in the DMZ. It terminates external TLS connections and creates new internal connections to ADFS. If the DMZ is compromised, ADFS can revoke the proxy trust certificate, immediately cutting off all external access. +- The **NetBird management/signal/relay server** also sits in the DMZ (or a separate public-facing segment), accessible to peers. + +--- + +## Server Roles Summary + +| Server | Network Zone | Domain Joined | Roles | +| --- | --- | --- | --- | +| Domain Controller | Corporate LAN | Yes (DC) | AD DS, DNS | +| ADFS Server | Corporate LAN | Yes (member) | ADFS, Duo ADFS Adapter | +| Web Application Proxy | DMZ | Optional (recommended: no) | Remote Access (WAP role) | +| NetBird Management | DMZ | No | NetBird management, signal, relay, STUN | + + +The ADFS role must not be installed on the domain controller. The domain controller holds the AD database and Kerberos keys; combining it with an internet-proxied service creates an unnecessary attack surface. + + +--- + +## Firewall Rules + +### DMZ to Internet (WAP and NetBird) + +| Source | Destination | Port | Protocol | Purpose | +| --- | --- | --- | --- | --- | +| Internet | WAP | 443 | TCP | ADFS authentication (OIDC login flow) | +| Internet | NetBird Mgmt | 443 | TCP | Dashboard, signal, peer management | +| Internet | NetBird Mgmt | 3478 | UDP | STUN (peer connection negotiation) | + +### DMZ to Corporate LAN + +| Source | Destination | Port | Protocol | Purpose | +| --- | --- | --- | --- | --- | +| WAP | ADFS Server | 443 | TCP | Proxy ADFS traffic to internal server | + +No other traffic from the DMZ should reach the corporate LAN. This is the critical boundary. + +### Corporate LAN Internal + +| Source | Destination | Port | Protocol | Purpose | +| --- | --- | --- | --- | --- | +| ADFS Server | Domain Controller | 389/636 | TCP | LDAP/LDAPS for authentication | +| ADFS Server | Domain Controller | 88 | TCP/UDP | Kerberos | +| ADFS Server | Domain Controller | 53 | TCP/UDP | DNS | +| ADFS Server | Duo Cloud (`*.duosecurity.com`) | 443 | TCP | Duo MFA verification (outbound) | + +--- + +## Prerequisites + +- Windows Server 2016 or later for the ADFS server (Windows Server 2025 recommended) +- Windows Server 2016 or later for the WAP server (can be a different version than the ADFS server) +- A functioning Active Directory domain with user accounts +- A TLS certificate for the ADFS service name (e.g., `adfs.example.com`), issued by a CA trusted by both internal and external clients. The same certificate must be installed on both the ADFS server and the WAP server. +- A Cisco Duo account (Essentials, Advantage, or Premier plan) +- A deployed NetBird self-hosted instance +- DNS configured so that: + - External DNS resolves the ADFS service name to the WAP's public IP (or DMZ load balancer) + - Internal DNS resolves the ADFS service name to the ADFS server's internal IP (or internal load balancer) + +### Duo Authentication Proxy vs. Duo ADFS Adapter + +If your organization currently uses the Duo Authentication Proxy for RADIUS or LDAP-based MFA (for example, for VPN access), note that the **Duo ADFS MFA Adapter** is a separate product. Both coexist under the same Duo tenant, and users already enrolled in Duo do not need to re-enroll. The Duo Auth Proxy continues to function as-is for its existing integrations. + +The Duo Authentication Proxy does not support OIDC. It only handles RADIUS and LDAP, and cannot serve as an identity provider for NetBird. + +--- + +## Active Directory: User Attribute Requirements + +ADFS will pull user attributes from Active Directory and include them as claims in the OIDC tokens sent to NetBird. The following AD attributes must be populated for each user: + +| AD Attribute | Maps to OIDC Claim | Purpose in NetBird | +| --- | --- | --- | +| `mail` | `email` | User email address | +| `displayName` | `name` | Display name in the dashboard | +| `givenName` | `given_name` | First name | +| `sn` | `family_name` | Last name | +| `userPrincipalName` | `upn` | User identifier (see [Step 7.2](#72-required-netbird-configuration-settings)) | + +Verify that your user objects have these attributes populated: + +```powershell +Get-ADUser -Identity -Properties displayName, givenName, sn, mail | Format-List +``` + +If `displayName`, `givenName`, or `sn` are empty, NetBird will fall back to displaying the UPN as the user name. Populate these attributes before proceeding: + +```powershell +Set-ADUser -Identity -GivenName "First" -Surname "Last" -DisplayName "First Last" +``` + +### Optional: Custom UPN Suffix + +By default, user UPNs use the internal AD domain (e.g., `alice@corp.example.local`). If you prefer a cleaner login experience using your public domain: + +```powershell +Get-ADForest | Set-ADForest -UPNSuffixes @{Add="example.com"} +Set-ADUser -Identity -UserPrincipalName "alice@example.com" +``` + + +Changing UPNs in a production environment can affect other services that depend on them (Kerberos delegation, certificate-based authentication, federated services). Evaluate the impact before modifying existing user UPNs. + + +--- + +## Step 1: Install and Configure ADFS on the Member Server + +### 1.1 Install the ADFS Role + +On the dedicated ADFS member server (not the domain controller): + +```powershell +Install-WindowsFeature ADFS-Federation -IncludeManagementTools +``` + +### 1.2 Install the TLS Certificate + +Import your CA-issued TLS certificate into the Local Computer Personal store (`Cert:\LocalMachine\My`). Note the thumbprint. + +If the certificate was issued as a PFX file: + +```powershell +$pfxPassword = ConvertTo-SecureString "your-pfx-password" -AsPlainText -Force +Import-PfxCertificate -FilePath "C:\path\to\adfs.pfx" ` + -CertStoreLocation "Cert:\LocalMachine\My" ` + -Password $pfxPassword +``` + +Note the thumbprint: + +```powershell +Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Subject -like "*adfs*" } | Format-List Subject, Thumbprint, NotAfter +``` + +### 1.3 Create a Group Managed Service Account (gMSA) + +Microsoft recommends using a gMSA for the ADFS service account in production deployments: + +```powershell +# Run on the domain controller first (one-time setup) +Add-KdsRootKey -EffectiveImmediately + +# Then create the gMSA (can be run from the ADFS server) +New-ADServiceAccount -Name "svc-adfs" ` + -DnsHostName "adfs.example.com" ` + -PrincipalsAllowedToRetrieveManagedPassword "ADFS-Server$" +``` + +Replace `ADFS-Server$` with the computer account name of your ADFS member server. If you have multiple ADFS servers in a farm, use a group containing all of their computer accounts. + +Install the gMSA on the ADFS server: + +```powershell +Install-ADServiceAccount -Identity "svc-adfs" +Test-ADServiceAccount -Identity "svc-adfs" # Should return True +``` + +### 1.4 Configure the ADFS Farm + +```powershell +$certThumbprint = "" + +Install-AdfsFarm ` + -CertificateThumbprint $certThumbprint ` + -FederationServiceName "adfs.example.com" ` + -FederationServiceDisplayName "Corporate ADFS" ` + -GroupServiceAccountIdentifier "CORP\svc-adfs$" ` + -OverwriteConfiguration +``` + +After the command completes, restart the server: + +```powershell +Restart-Computer +``` + +### 1.5 Verify ADFS is Running + +```powershell +Get-Service adfssrv + +Invoke-WebRequest -Uri "https://adfs.example.com/adfs/.well-known/openid-configuration" ` + -UseBasicParsing | Select-Object -ExpandProperty Content +``` + + +If the ADFS server resolves its own service name to an external IP (for example, through split DNS or public DNS), you may need to add a hosts file entry pointing the ADFS FQDN to `127.0.0.1` and flush DNS (`ipconfig /flushdns`) for local testing to work. + + +--- + +## Step 2: Configure the OIDC Application Group + +### 2.1 Generate a Client ID + +```powershell +$clientId = [guid]::NewGuid().ToString() +Write-Host "Client ID: $clientId" +``` + +Record this value. You will use it in multiple steps and when configuring NetBird. + +### 2.2 Create the Application Group + +```powershell +New-AdfsApplicationGroup -Name "NetBird" -ApplicationGroupIdentifier "NetBird" +``` + +### 2.3 Add a Native Application (Public Client) + + +The NetBird dashboard is a browser-based single-page application (SPA) that uses PKCE (Proof Key for Code Exchange) for authentication. ADFS must be configured with a **Native Application** (public client), not a Server Application (confidential client). A Server Application expects a `client_secret` on every token exchange, which an SPA cannot provide. Using the wrong client type will result in a 400 error on every login attempt. + + +```powershell +Add-AdfsNativeClientApplication ` + -Name "NetBird - Native App" ` + -ApplicationGroupIdentifier "NetBird" ` + -Identifier $clientId ` + -RedirectUri @( + "https:///peers", + "https:///add-peers", + "http://localhost:53000" + ) +``` + +Replace `` with your NetBird management URL. The redirect URIs must point to actual routes in the NetBird dashboard. The `http://localhost:53000` entry is required for CLI-based peer authentication. + +### 2.4 Add a Web API + +```powershell +Add-AdfsWebApiApplication ` + -Name "NetBird - Web API" ` + -ApplicationGroupIdentifier "NetBird" ` + -Identifier $clientId ` + -AccessControlPolicyName "Permit everyone" +``` + +### 2.5 Grant OIDC Permissions + +```powershell +Grant-AdfsApplicationPermission ` + -ClientRoleIdentifier $clientId ` + -ServerRoleIdentifier $clientId ` + -ScopeNames "openid","profile","email" +``` + +--- + +## Step 3: Configure Claim Transform Rules + +NetBird requires specific claims in the OIDC tokens. Add these issuance transform rules to the Web API: + +```powershell +# Rule 1: Send user attributes (email, first name, last name) +$ldapRule = @" +@RuleTemplate = "LdapClaims" +@RuleName = "Send LDAP Attributes" +c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", + Issuer == "AD AUTHORITY"] +=> issue(store = "Active Directory", + types = ("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"), + query = ";mail,givenName,sn;{0}", + param = c.Value); +"@ + +# Rule 2: Send display name +$nameRule = @" +@RuleName = "Send Display Name" +c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", + Issuer == "AD AUTHORITY"] +=> issue(store = "Active Directory", + types = ("name"), + query = ";displayName;{0}", + param = c.Value); +"@ + +# Rule 3a: Query all groups into a temporary claim type +$groupQueryRule = @" +@RuleName = "Query Group Membership" +c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", + Issuer == "AD AUTHORITY"] +=> issue(store = "Active Directory", + types = ("http://temp/groups"), + query = ";tokenGroups(unqualifiedName);{0}", + param = c.Value); +"@ + +# Rule 3b: Filter to only emit groups starting with "NetBird-" +# Adjust the regex pattern to match your naming convention. +# Examples: +# "^NetBird-" matches NetBird-Users, NetBird-Admins, etc. +# "^(NetBird-|Ops-)" matches NetBird- and Ops- prefixed groups +$groupFilterRule = @" +@RuleName = "Filter Group Membership" +c:[Type == "http://temp/groups", Value =~ "^NetBird-"] +=> issue(Type = "groups", Value = c.Value); +"@ + +# Rule 4: Send UPN +$upnRule = @" +@RuleTemplate = "LdapClaims" +@RuleName = "Send UPN" +c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", + Issuer == "AD AUTHORITY"] +=> issue(store = "Active Directory", + types = ("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"), + query = ";userPrincipalName;{0}", + param = c.Value); +"@ + +# Apply all rules +Set-AdfsWebApiApplication ` + -TargetName "NetBird - Web API" ` + -IssuanceTransformRules ($ldapRule + $nameRule + $groupQueryRule + $groupFilterRule + $upnRule) +``` + + +The `name` claim must be emitted in its own rule (Rule 2) using the short claim type `"name"`. Do not include `displayName` in Rule 1 using the full schema URI `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name`. ADFS implicitly emits a `name` claim containing the Windows account name (for example, `CORP\alice`). If Rule 1 also emits a `name` claim through the schema URI, the token will contain a `name` array with two values, which NetBird cannot parse correctly. + + + +**Why the UPN rule is required:** ADFS generates base64-encoded pairwise subject identifiers for the `sub` claim by default. Base64 encoding can produce `/`, `+`, and `=` characters. When NetBird uses the `sub` claim as the user identifier in API URL paths, the `/` characters are interpreted as path separators, causing 404 errors. The UPN claim provides a URL-safe, human-readable identifier instead. + + +Groups are filtered at the ADFS level so only relevant groups appear in the JWT and get synced into NetBird. + +--- + +## Step 4: Enable CORS + +The NetBird dashboard makes cross-origin requests to ADFS during the OIDC authentication flow. ADFS does not enable CORS by default, and without it the browser will silently block the token response even though ADFS returns a successful result. + +```powershell +Set-AdfsResponseHeaders -EnableCORS $true +Set-AdfsResponseHeaders -CORSTrustedOrigins @("https://") +``` + +--- + +## Step 5: Install and Configure Web Application Proxy + +The WAP server sits in the DMZ and proxies ADFS traffic from the internet to the internal ADFS server. External clients never connect directly to ADFS. If the DMZ is compromised, ADFS can revoke the proxy trust and immediately cut off all external access. + +### 5.1 Prepare the WAP Server + +The WAP server: + +- Should be in the DMZ, separated from the corporate network by a firewall +- Can be non-domain-joined for stronger isolation (recommended for high-security environments). If non-domain-joined, it must be able to resolve the ADFS service name to the ADFS server's internal IP. +- Needs the same TLS certificate installed that is used on the ADFS server +- Needs TCP 443 inbound from the internet and TCP 443 outbound to the ADFS server + +If non-domain-joined, add a hosts file entry on the WAP server so it can resolve the ADFS service name: + +``` + adfs.example.com +``` + +Import the TLS certificate into the WAP server's Personal store, the same way as on the ADFS server ([Step 1.2](#12-install-the-tls-certificate)). + +### 5.2 Install the WAP Role + +```powershell +Install-WindowsFeature Web-Application-Proxy -IncludeManagementTools +``` + +### 5.3 Configure WAP to Trust ADFS + +```powershell +$adfsCredential = Get-Credential +# Enter domain credentials that have local admin rights on the ADFS server. +# These are used once to establish the proxy trust and are not stored. + +Install-WebApplicationProxy ` + -CertificateThumbprint "" ` + -FederationServiceName "adfs.example.com" ` + -FederationServiceTrustCredential $adfsCredential +``` + +### 5.4 Verify the Proxy Trust + +From the WAP server: + +```powershell +Get-WebApplicationProxyHealth +``` + +All components should show as healthy. If not, verify: + +- TCP 443 is open from the WAP to the ADFS server through the internal firewall +- The WAP can resolve `adfs.example.com` to the ADFS server's internal IP +- The TLS certificate on the WAP matches the one on the ADFS server + +### 5.5 Test External Access + +From an external machine, navigate to: + +``` +https://adfs.example.com/adfs/.well-known/openid-configuration +``` + +DNS should resolve to the WAP's public IP. The WAP proxies the request to the internal ADFS server, and you should see the OIDC discovery JSON. + +--- + +## Step 6: Install and Configure the Duo ADFS MFA Adapter + +The Duo adapter is installed on the **ADFS server** (internal network), not on the WAP. + +### 6.1 Create the Application in Duo + +1. Log into the Duo Admin Panel at [https://admin.duosecurity.com](https://admin.duosecurity.com) +2. Navigate to **Applications > Protect an Application** +3. Search for **Microsoft ADFS** and click **Protect** +4. Record the **Client ID**, **Client Secret**, and **API Hostname** +5. Under the **User access** section, select **Enable for all users** (or configure per your access policy) + +### 6.2 Install the Adapter + +Download the latest Duo ADFS adapter from [https://dl.duosecurity.com/duo-adfs3-latest.msi](https://dl.duosecurity.com/duo-adfs3-latest.msi) and run it on the ADFS server as an administrator. + +When prompted, provide the Client ID, Client Secret, and API Hostname from the Duo Admin Panel. + + +**Windows Server 2025:** Duo ADFS adapter v2.3.0 or later is required. + + + +**Fail-closed recommendation:** For production deployments, leave **"Bypass Duo authentication when offline"** unchecked. If the ADFS server cannot reach Duo's cloud, authentication should fail rather than bypass MFA. The trade-off is that legitimate users will also be locked out during a Duo outage. + + + +**Outbound connectivity requirement:** The ADFS server must be able to reach `*.duosecurity.com` on TCP 443. Ensure your egress firewall rules permit this. The Duo adapter does not function without cloud connectivity. + + +### 6.3 Enable Duo in ADFS Authentication Methods + +1. Open **AD FS Management** on the ADFS server +2. Navigate to **Service > Authentication Methods** +3. Click **Edit** under "Multi-factor Authentication Methods" +4. On the **Additional** tab, check **Duo Authentication for AD FS** +5. Click **OK** + +### 6.4 Configure an MFA Policy + +To require MFA for all logins: + +```powershell +Set-AdfsAdditionalAuthenticationRule ` + -AdditionalAuthenticationRules '=> issue(Type = "http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod", Value = "http://schemas.microsoft.com/claims/multipleauthn");' +``` + +For more granular policies (for example, MFA only for extranet access, specific groups, or specific relying parties), refer to the [Microsoft AD FS MFA documentation](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/operations/configure-additional-authentication-methods-for-ad-fs) and the [Duo AD FS advanced configuration guide](https://duo.com/docs/adfs). + + +ADFS can differentiate between requests originating from the corporate network (via direct access) and requests coming from the internet (via the WAP). This allows you to enforce MFA only for external access while allowing internal users to authenticate without a second factor. This is configured through AD FS access control policies. + + +### 6.5 Test the MFA Flow + +Enable the IdP-initiated sign-on page for testing: + +```powershell +Set-AdfsProperties -EnableIdPInitiatedSignOnPage $true +``` + +From an external machine, navigate to `https://adfs.example.com/adfs/ls/idpinitiatedsignon`, sign in with an AD account, and verify that the Duo MFA prompt appears and completes successfully. This traffic should route through the WAP. + +--- + +## Step 7: Configure NetBird + +### 7.1 OIDC Provider Values + +Provide the following values during NetBird setup or in your NetBird configuration: + +| Field | Value | +| --- | --- | +| OIDC Discovery Endpoint | `https:///adfs/.well-known/openid-configuration` | +| Client ID | The GUID generated in [Step 2.1](#21-generate-a-client-id) | +| Scopes | `openid profile email` | + +### 7.2 Required NetBird Configuration Settings + +Two settings are required for correct operation with ADFS: + +```bash +NETBIRD_TOKEN_SOURCE="idToken" +NETBIRD_AUTH_USER_ID_CLAIM="upn" +``` + +**`NETBIRD_TOKEN_SOURCE=idToken`** — ADFS includes user claims (email, name, groups) in the `id_token`. The `access_token` may not contain these claims. Without this setting, users may appear without names or email addresses in the NetBird dashboard. + +**`NETBIRD_AUTH_USER_ID_CLAIM=upn`** — This tells NetBird to use the UPN claim as the user identifier instead of the default `sub` claim. The ADFS `sub` claim contains base64 characters that break URL routing in the management API. + +### 7.3 Redirect URI Configuration + +After NetBird is deployed, verify that the redirect URIs configured in the ADFS Native Application ([Step 2.3](#23-add-a-native-application-public-client)) exactly match the URIs that NetBird sends in its OIDC authorization requests. Mismatches in paths, trailing slashes, or protocol will cause login failures. + +To check the currently configured redirect URIs: + +```powershell +Get-AdfsNativeClientApplication -Name "NetBird - Native App" | Select-Object -ExpandProperty RedirectUri +``` + +To update them: + +```powershell +Set-AdfsNativeClientApplication ` + -TargetName "NetBird - Native App" ` + -RedirectUri @( + "https:///peers", + "https:///add-peers", + "http://localhost:53000" + ) +``` + +### 7.4 JWT Group Sync + +To synchronize AD group memberships into NetBird for use in access control policies: + +1. In the NetBird dashboard, navigate to **Settings > Groups** +2. Toggle **Enable JWT group sync** +3. Set the JWT claim name to `groups` + +Groups appear in NetBird using the short names from the token (for example, `NetBird-Users`, `Domain Users`). Groups are synced at login time — when a user authenticates, their current group memberships are read from the token. Only groups matching the ADFS filter pattern will appear in NetBird, and that filter is controlled by the regex in the "Filter Group Membership" claim rule. + +### 7.5 TLS Verification + +The NetBird management server must trust the TLS certificate on the ADFS/WAP endpoint. If you are using a certificate from a public CA, this works automatically. If you are using an internal CA, add the CA certificate to the trust store on the NetBird server. + +Verify from the NetBird server: + +```bash +curl -s https:///adfs/.well-known/openid-configuration | jq . +``` + +This request routes through the WAP and should return the OIDC discovery document with no TLS errors. + +--- + +## Verification + +### Test the ADFS OIDC Endpoint (through WAP) + +From an external machine: + +``` +https:///adfs/.well-known/openid-configuration +``` + +Confirm the response includes `issuer`, `authorization_endpoint`, `token_endpoint`, and `jwks_uri`. + +### Test the Duo MFA Flow (through WAP) + +Navigate to `https:///adfs/ls/idpinitiatedsignon` from an external machine. Sign in with an AD account and verify that the Duo MFA prompt appears. + +### Test the Full NetBird Login + +1. Open the NetBird dashboard +2. The login page should redirect to ADFS (through the WAP) +3. Enter AD credentials +4. Complete the Duo MFA challenge +5. Confirm you are redirected back to the NetBird dashboard as the authenticated user +6. Verify the user's display name, email, and group memberships appear correctly + +--- + +## Troubleshooting + +### Login returns a 400 error on token exchange + +The ADFS application is configured as a Server Application (confidential client) instead of a Native Application (public client). The NetBird dashboard uses PKCE and does not send a `client_secret`. Recreate the application using `Add-AdfsNativeClientApplication` per [Step 2.3](#23-add-a-native-application-public-client), then re-grant permissions per [Step 2.5](#25-grant-oidc-permissions). + +### Dashboard login silently fails with no error + +CORS is not configured on ADFS. The browser is blocking the cross-origin token response. Run the commands in [Step 4](#step-4-enable-cors). + +### "Access denied: Your Duo account doesn't have access to this application" + +In the Duo Admin Panel, open the Microsoft ADFS application and set User access to "Enable for all users", or add the relevant Duo groups. + +### Users appear without a name or email in NetBird + +Verify that the AD user objects have `displayName`, `givenName`, `sn`, and `mail` attributes populated. Also verify that `NETBIRD_TOKEN_SOURCE` is set to `idToken` in the NetBird configuration. + +### User display name shows as "CORP" or an array instead of the real name + +The ADFS `name` claim contains two values: one from the implicit Windows account name and one from your LDAP rule. Reconfigure the claim rules per [Step 3](#step-3-configure-claim-transform-rules), using a dedicated rule (Rule 2) that emits `displayName` with the short claim type `"name"` instead of the schema URI. + +### NetBird API returns 404 for user operations + +The ADFS `sub` claim contains base64 characters (`/`, `+`, `=`) that break URL path routing. Set `NETBIRD_AUTH_USER_ID_CLAIM=upn` in the NetBird configuration and verify the UPN claim rule exists in the ADFS Web API (Step 3, Rule 4). + +### Redirect URI mismatch errors + +The redirect URIs in the ADFS Native Application must exactly match what NetBird sends. Check the `redirect_uri` parameter in the browser's network tab during a login attempt and compare it to the ADFS configuration. Update with `Set-AdfsNativeClientApplication` if they differ. + +### WAP cannot connect to ADFS + +Verify that TCP 443 is open from WAP to ADFS through the internal firewall, that the WAP can resolve the ADFS service name to the ADFS server's internal IP, and that the TLS certificate on the WAP matches the one on the ADFS server (same thumbprint). Run `Get-WebApplicationProxyHealth` on the WAP to diagnose. + +### OIDC discovery endpoint unreachable from the ADFS server itself + +If the ADFS server resolves its own FQDN to an external IP, loopback traffic may fail depending on your network configuration. Add a hosts file entry mapping the ADFS FQDN to `127.0.0.1` on the ADFS server, then run `ipconfig /flushdns`. + +--- + +## Reference: Configuration Summary + +### ADFS Configuration + +| Component | Type | Name | +| --- | --- | --- | +| Application Group | - | NetBird | +| Native Application | Public client (PKCE) | NetBird - Native App | +| Web API | Resource | NetBird - Web API | +| Claim Rules | Issuance Transform | Send LDAP Attributes, Send Display Name, Query Group Membership, Filter Group Membership, Send UPN | +| CORS | Trusted Origin | `https://` | +| MFA | Additional Authentication | Duo Authentication for AD FS | + +### OIDC Values for NetBird + +| Field | Value | +| --- | --- | +| Issuer | `https:///adfs` | +| Discovery Endpoint | `https:///adfs/.well-known/openid-configuration` | +| Client ID | `` | +| Scopes | `openid profile email` | +| Token Source | `idToken` | +| User ID Claim | `upn` | + +--- + +## Related Resources + +- [Microsoft AD FS documentation](https://learn.microsoft.com/en-us/windows-server/identity/active-directory-federation-services) +- [Microsoft Web Application Proxy documentation](https://learn.microsoft.com/en-us/windows-server/remote/remote-access/web-application-proxy/web-application-proxy-in-windows-server) +- [Duo AD FS documentation](https://duo.com/docs/adfs) +- [NetBird Generic OIDC reference](/selfhosted/identity-providers/generic-oidc) From 620c0600089fc92d1ad4154231984d6a55192a05 Mon Sep 17 00:00:00 2001 From: Jack Carter <128555021+SunsetDrifter@users.noreply.github.com> Date: Fri, 17 Apr 2026 12:11:25 +0200 Subject: [PATCH 2/7] docs: rewrite ADFS guide for Community Edition Dashboard flow Switch from standalone/setup.env style to the CE-native Dashboard-based external IdP flow: - Use a confidential Server Application (Add-AdfsServerApplication with generated client secret) instead of a Native Application with PKCE. - Redirect URI now comes from NetBird's Settings > Identity Providers flow, not hard-coded /peers paths. - Drop the NETBIRD_TOKEN_SOURCE and NETBIRD_AUTH_USER_ID_CLAIM env vars (those are standalone/commercial-license settings). - Fix the base64 sub claim issue upstream in ADFS via a new claim rule (Rule 5) that emits sub from UPN, with a fallback note about PairwiseIdentifierEnabled for ADFS builds that need it. - Update Troubleshooting and Configuration Summary to match. --- .../selfhosted/identity-providers/adfs.mdx | 153 ++++++++++-------- 1 file changed, 90 insertions(+), 63 deletions(-) diff --git a/src/pages/selfhosted/identity-providers/adfs.mdx b/src/pages/selfhosted/identity-providers/adfs.mdx index 4b866ad3..718264e5 100644 --- a/src/pages/selfhosted/identity-providers/adfs.mdx +++ b/src/pages/selfhosted/identity-providers/adfs.mdx @@ -258,6 +258,8 @@ If the ADFS server resolves its own service name to an external IP (for example, ## Step 2: Configure the OIDC Application Group +You'll configure ADFS as an external OIDC identity provider that NetBird can add via **Settings > Identity Providers** in the Management Dashboard. NetBird generates a redirect URL that must be registered in ADFS, so you'll pause mid-way through this step to fetch that URL from the NetBird Dashboard. + ### 2.1 Generate a Client ID ```powershell @@ -273,27 +275,42 @@ Record this value. You will use it in multiple steps and when configuring NetBir New-AdfsApplicationGroup -Name "NetBird" -ApplicationGroupIdentifier "NetBird" ``` -### 2.3 Add a Native Application (Public Client) +### 2.3 Get the Redirect URL from NetBird - -The NetBird dashboard is a browser-based single-page application (SPA) that uses PKCE (Proof Key for Code Exchange) for authentication. ADFS must be configured with a **Native Application** (public client), not a Server Application (confidential client). A Server Application expects a `client_secret` on every token exchange, which an SPA cannot provide. Using the wrong client type will result in a 400 error on every login attempt. - +1. Log in to your NetBird Dashboard in another browser tab +2. Navigate to **Settings > Identity Providers** +3. Click **Add Identity Provider** +4. Select **Generic OIDC** from the type dropdown +5. Fill in: + + | Field | Value | + | --- | --- | + | Name | `ADFS` (or your preferred display name) | + | Client ID | The GUID from [Step 2.1](#21-generate-a-client-id) | + | Client Secret | Enter a placeholder (you will replace it in [Step 7](#step-7-complete-netbird-configuration)) | + | Issuer | `https:///adfs` | + +6. NetBird displays a **Redirect URL** — copy this value. Do **not** click **Save** yet; you'll return to this tab in [Step 7](#step-7-complete-netbird-configuration). + +### 2.4 Add a Server Application (Confidential Client) + +Create a confidential OIDC client in ADFS with a generated client secret. NetBird's Dashboard IdP flow exchanges the authorization code using the client secret, so ADFS must be configured as a **Server Application**, not a Native Application. ```powershell -Add-AdfsNativeClientApplication ` - -Name "NetBird - Native App" ` +$serverApp = Add-AdfsServerApplication ` + -Name "NetBird - Server App" ` -ApplicationGroupIdentifier "NetBird" ` -Identifier $clientId ` - -RedirectUri @( - "https:///peers", - "https:///add-peers", - "http://localhost:53000" - ) + -RedirectUri @("") ` + -GenerateClientSecret + +$clientSecret = $serverApp.ClientSecret +Write-Host "Client Secret: $clientSecret" ``` -Replace `` with your NetBird management URL. The redirect URIs must point to actual routes in the NetBird dashboard. The `http://localhost:53000` entry is required for CLI-based peer authentication. +Replace `` with the URL you copied in [Step 2.3](#23-get-the-redirect-url-from-netbird). Record the client secret — you'll paste it into NetBird in [Step 7](#step-7-complete-netbird-configuration). ADFS does not display the secret again, so if you lose it you'll need to regenerate it with `Set-AdfsServerApplication ... -GenerateClientSecret`. -### 2.4 Add a Web API +### 2.5 Add a Web API ```powershell Add-AdfsWebApiApplication ` @@ -303,7 +320,7 @@ Add-AdfsWebApiApplication ` -AccessControlPolicyName "Permit everyone" ``` -### 2.5 Grant OIDC Permissions +### 2.6 Grant OIDC Permissions ```powershell Grant-AdfsApplicationPermission ` @@ -378,10 +395,17 @@ c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccou param = c.Value); "@ +# Rule 5: Override the default sub claim with the UPN +$subRule = @" +@RuleName = "Override sub claim" +c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"] +=> issue(Type = "sub", Value = c.Value); +"@ + # Apply all rules Set-AdfsWebApiApplication ` -TargetName "NetBird - Web API" ` - -IssuanceTransformRules ($ldapRule + $nameRule + $groupQueryRule + $groupFilterRule + $upnRule) + -IssuanceTransformRules ($ldapRule + $nameRule + $groupQueryRule + $groupFilterRule + $upnRule + $subRule) ``` @@ -389,7 +413,7 @@ The `name` claim must be emitted in its own rule (Rule 2) using the short claim -**Why the UPN rule is required:** ADFS generates base64-encoded pairwise subject identifiers for the `sub` claim by default. Base64 encoding can produce `/`, `+`, and `=` characters. When NetBird uses the `sub` claim as the user identifier in API URL paths, the `/` characters are interpreted as path separators, causing 404 errors. The UPN claim provides a URL-safe, human-readable identifier instead. +**Why the sub override (Rule 5) is required:** ADFS generates base64-encoded pairwise subject identifiers for the `sub` claim by default. Base64 encoding can produce `/`, `+`, and `=` characters. When NetBird uses the `sub` claim as the user identifier in API URL paths, the `/` characters are interpreted as path separators, causing 404 errors. Emitting `sub` from the UPN (Rule 5) provides a URL-safe, human-readable identifier instead. On some ADFS builds you may also need to disable pairwise identifiers on the Server Application (`Set-AdfsServerApplication ... -PairwiseIdentifierEnabled $false`) — check Microsoft's [AD FS OpenID Connect documentation](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-openid-connect-oauth-flows-scenarios) for your version. Groups are filtered at the ADFS level so only relevant groups appear in the JWT and get synced into NetBird. @@ -538,64 +562,56 @@ From an external machine, navigate to `https://adfs.example.com/adfs/ls/idpiniti --- -## Step 7: Configure NetBird +## Step 7: Complete NetBird Configuration -### 7.1 OIDC Provider Values +### 7.1 Finish Adding the Identity Provider -Provide the following values during NetBird setup or in your NetBird configuration: +Return to the NetBird Dashboard tab you opened in [Step 2.3](#23-get-the-redirect-url-from-netbird): -| Field | Value | -| --- | --- | -| OIDC Discovery Endpoint | `https:///adfs/.well-known/openid-configuration` | -| Client ID | The GUID generated in [Step 2.1](#21-generate-a-client-id) | -| Scopes | `openid profile email` | +1. Replace the placeholder **Client Secret** field with the secret generated in [Step 2.4](#24-add-a-server-application-confidential-client) +2. Verify the other values: -### 7.2 Required NetBird Configuration Settings + | Field | Value | + | --- | --- | + | Type | Generic OIDC | + | Name | `ADFS` (or your chosen display name) | + | Client ID | The GUID from [Step 2.1](#21-generate-a-client-id) | + | Client Secret | From [Step 2.4](#24-add-a-server-application-confidential-client) | + | Issuer | `https:///adfs` | -Two settings are required for correct operation with ADFS: +3. Click **Save** -```bash -NETBIRD_TOKEN_SOURCE="idToken" -NETBIRD_AUTH_USER_ID_CLAIM="upn" -``` - -**`NETBIRD_TOKEN_SOURCE=idToken`** — ADFS includes user claims (email, name, groups) in the `id_token`. The `access_token` may not contain these claims. Without this setting, users may appear without names or email addresses in the NetBird dashboard. - -**`NETBIRD_AUTH_USER_ID_CLAIM=upn`** — This tells NetBird to use the UPN claim as the user identifier instead of the default `sub` claim. The ADFS `sub` claim contains base64 characters that break URL routing in the management API. - -### 7.3 Redirect URI Configuration +### 7.2 Verify the Redirect URI -After NetBird is deployed, verify that the redirect URIs configured in the ADFS Native Application ([Step 2.3](#23-add-a-native-application-public-client)) exactly match the URIs that NetBird sends in its OIDC authorization requests. Mismatches in paths, trailing slashes, or protocol will cause login failures. - -To check the currently configured redirect URIs: +If you misplace the Redirect URL later, you can list the URIs currently registered on the ADFS Server Application: ```powershell -Get-AdfsNativeClientApplication -Name "NetBird - Native App" | Select-Object -ExpandProperty RedirectUri +Get-AdfsServerApplication -Name "NetBird - Server App" | Select-Object -ExpandProperty RedirectUri ``` -To update them: +To update them (for example, if NetBird regenerates the URL after reconfiguration): ```powershell -Set-AdfsNativeClientApplication ` - -TargetName "NetBird - Native App" ` - -RedirectUri @( - "https:///peers", - "https:///add-peers", - "http://localhost:53000" - ) +Set-AdfsServerApplication ` + -TargetName "NetBird - Server App" ` + -RedirectUri @("") ``` -### 7.4 JWT Group Sync +### 7.3 Enable JWT Group Sync To synchronize AD group memberships into NetBird for use in access control policies: -1. In the NetBird dashboard, navigate to **Settings > Groups** +1. In the NetBird Dashboard, navigate to **Settings > Groups** 2. Toggle **Enable JWT group sync** 3. Set the JWT claim name to `groups` Groups appear in NetBird using the short names from the token (for example, `NetBird-Users`, `Domain Users`). Groups are synced at login time — when a user authenticates, their current group memberships are read from the token. Only groups matching the ADFS filter pattern will appear in NetBird, and that filter is controlled by the regex in the "Filter Group Membership" claim rule. -### 7.5 TLS Verification + +Groups with matching names in NetBird and ADFS will **not** sync. To import a group from ADFS, first delete the existing group with that name in NetBird. + + +### 7.4 TLS Verification The NetBird management server must trust the TLS certificate on the ADFS/WAP endpoint. If you are using a certificate from a public CA, this works automatically. If you are using an internal CA, add the CA certificate to the trust store on the NetBird server. @@ -607,6 +623,15 @@ curl -s https:///adfs/.well-known/openid-configuration | jq . This request routes through the WAP and should return the OIDC discovery document with no TLS errors. +### 7.5 Test the Login + +1. Log out of the NetBird Dashboard +2. On the login page, click the **ADFS** button +3. You are redirected to ADFS (through the WAP) +4. Sign in with AD credentials and complete the Duo MFA challenge +5. Confirm you are redirected back to the NetBird Dashboard as the authenticated user +6. Verify the user's display name, email, and group memberships appear correctly + --- ## Verification @@ -640,7 +665,7 @@ Navigate to `https:///adfs/ls/idpinitiatedsignon` from an external ma ### Login returns a 400 error on token exchange -The ADFS application is configured as a Server Application (confidential client) instead of a Native Application (public client). The NetBird dashboard uses PKCE and does not send a `client_secret`. Recreate the application using `Add-AdfsNativeClientApplication` per [Step 2.3](#23-add-a-native-application-public-client), then re-grant permissions per [Step 2.5](#25-grant-oidc-permissions). +The ADFS application is most likely configured as a Native Application (public client) instead of a Server Application (confidential client). NetBird's Dashboard IdP flow sends a `client_secret` during token exchange, which ADFS will reject if the app was created with `Add-AdfsNativeClientApplication`. Delete the Native Application and recreate it using `Add-AdfsServerApplication` per [Step 2.4](#24-add-a-server-application-confidential-client), then re-grant permissions per [Step 2.6](#26-grant-oidc-permissions). Also verify the client secret pasted into NetBird matches the one generated in ADFS. ### Dashboard login silently fails with no error @@ -652,7 +677,7 @@ In the Duo Admin Panel, open the Microsoft ADFS application and set User access ### Users appear without a name or email in NetBird -Verify that the AD user objects have `displayName`, `givenName`, `sn`, and `mail` attributes populated. Also verify that `NETBIRD_TOKEN_SOURCE` is set to `idToken` in the NetBird configuration. +Verify that the AD user objects have `displayName`, `givenName`, `sn`, and `mail` attributes populated, and that the LDAP and Display Name claim rules from [Step 3](#step-3-configure-claim-transform-rules) are attached to the Web API. Decode a fresh id\_token with a JWT inspector to confirm `email`, `name`, `given_name`, and `family_name` are present. ### User display name shows as "CORP" or an array instead of the real name @@ -660,11 +685,11 @@ The ADFS `name` claim contains two values: one from the implicit Windows account ### NetBird API returns 404 for user operations -The ADFS `sub` claim contains base64 characters (`/`, `+`, `=`) that break URL path routing. Set `NETBIRD_AUTH_USER_ID_CLAIM=upn` in the NetBird configuration and verify the UPN claim rule exists in the ADFS Web API (Step 3, Rule 4). +The ADFS `sub` claim contains base64 characters (`/`, `+`, `=`) that break URL path routing. Verify that the "Override sub claim" rule (Rule 5 in [Step 3](#step-3-configure-claim-transform-rules)) is active on the Web API. On some ADFS versions the default pairwise identifier is still emitted alongside your override; in that case disable pairwise identifiers on the Server Application with `Set-AdfsServerApplication ... -PairwiseIdentifierEnabled $false`. ### Redirect URI mismatch errors -The redirect URIs in the ADFS Native Application must exactly match what NetBird sends. Check the `redirect_uri` parameter in the browser's network tab during a login attempt and compare it to the ADFS configuration. Update with `Set-AdfsNativeClientApplication` if they differ. +The redirect URI in the ADFS Server Application must exactly match the one NetBird displays in **Settings > Identity Providers**. Check the `redirect_uri` parameter in the browser's network tab during a login attempt and compare it to the ADFS configuration. Update with `Set-AdfsServerApplication -TargetName "NetBird - Server App" -RedirectUri @("")` if they differ. ### WAP cannot connect to ADFS @@ -683,22 +708,24 @@ If the ADFS server resolves its own FQDN to an external IP, loopback traffic may | Component | Type | Name | | --- | --- | --- | | Application Group | - | NetBird | -| Native Application | Public client (PKCE) | NetBird - Native App | +| Server Application | Confidential client (client secret) | NetBird - Server App | | Web API | Resource | NetBird - Web API | -| Claim Rules | Issuance Transform | Send LDAP Attributes, Send Display Name, Query Group Membership, Filter Group Membership, Send UPN | +| Claim Rules | Issuance Transform | Send LDAP Attributes, Send Display Name, Query Group Membership, Filter Group Membership, Send UPN, Override sub claim | | CORS | Trusted Origin | `https://` | | MFA | Additional Authentication | Duo Authentication for AD FS | -### OIDC Values for NetBird +### NetBird Identity Provider Values + +Entered in **Settings > Identity Providers > Add Identity Provider** in the NetBird Dashboard. | Field | Value | | --- | --- | +| Type | Generic OIDC | +| Name | `ADFS` (display name on the login page) | | Issuer | `https:///adfs` | -| Discovery Endpoint | `https:///adfs/.well-known/openid-configuration` | -| Client ID | `` | -| Scopes | `openid profile email` | -| Token Source | `idToken` | -| User ID Claim | `upn` | +| Client ID | The GUID generated in [Step 2.1](#21-generate-a-client-id) | +| Client Secret | Generated by `Add-AdfsServerApplication` in [Step 2.4](#24-add-a-server-application-confidential-client) | +| Redirect URL | Generated by NetBird; registered in ADFS in [Step 2.4](#24-add-a-server-application-confidential-client) | --- From 546d6ef41cd3bd081245382323db78199bbb7a67 Mon Sep 17 00:00:00 2001 From: Jack Carter <128555021+SunsetDrifter@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:14:43 +0200 Subject: [PATCH 3/7] docs: expand ADFS Step 1 and Step 5 with deeper setup prose Pull in the richer explanations from the updated source guide: - Step 1 gets server-provisioning prerequisites, Get-WindowsFeature verification after role install, expanded TLS cert rationale with Test-Certificate, a three-option service-account discussion with the Get-KdsRootKey check and lab-mode EffectiveTime trick, a full troubleshooting block for Install-ADServiceAccount, per-parameter explanations for Install-AdfsFarm, and a Start-Service + event-log fallback plus detailed OIDC-endpoint troubleshooting in 1.5. - Step 5 gets a full Provision the WAP Server section covering server specs, the domain-join decision (with SCADA framing generalized), pre-install firewall rules, hosts-file name resolution with Test- NetConnection, and exact Export-PfxCertificate/Import-PfxCertificate flow for the WAP cert. Step 5.3 is reframed as Establish the Proxy Trust with what-it-does and what-you-need callouts; 5.4 expands Get-WebApplicationProxyHealth troubleshooting. CE-specific rewrites (Server Application flow, Dashboard IdP config, Rule 5 sub override, Duo-optional framing) are preserved. --- .../selfhosted/identity-providers/adfs.mdx | 340 +++++++++++++++--- 1 file changed, 293 insertions(+), 47 deletions(-) diff --git a/src/pages/selfhosted/identity-providers/adfs.mdx b/src/pages/selfhosted/identity-providers/adfs.mdx index 718264e5..daf3da9f 100644 --- a/src/pages/selfhosted/identity-providers/adfs.mdx +++ b/src/pages/selfhosted/identity-providers/adfs.mdx @@ -4,7 +4,7 @@ import {Note} from "@/components/mdx"; [Active Directory Federation Services (ADFS)](https://learn.microsoft.com/en-us/windows-server/identity/active-directory-federation-services) is Microsoft's on-premises identity federation service. It lets organizations use their existing Active Directory accounts for single sign-on to external applications via OpenID Connect, OAuth 2.0, and SAML 2.0. ADFS is a common choice when on-prem AD is already the source of truth for user identity and the organization does not want to move authentication to a cloud IdP. -This guide walks through deploying ADFS as an OIDC identity provider for a self-hosted NetBird instance, following Microsoft's recommended topology (dedicated ADFS member server fronted by a Web Application Proxy in a DMZ) with Cisco Duo MFA enforced at authentication. +This guide walks through deploying ADFS as an OIDC identity provider for a self-hosted NetBird instance, following Microsoft's recommended topology (dedicated ADFS member server fronted by a Web Application Proxy in a DMZ). It also covers enforcing Cisco Duo MFA at authentication as an optional hardening step — you can skip [Step 6](#step-6-install-and-configure-the-duo-adfs-mfa-adapter) if you don't need MFA or plan to enforce it through another mechanism. ## Overview @@ -13,7 +13,7 @@ This guide walks through deploying ADFS as an OIDC identity provider for a self- - Deploying ADFS on a dedicated member server (separate from the domain controller) - Deploying Web Application Proxy (WAP) in a DMZ to front ADFS for external access - Configuring an ADFS OIDC application for NetBird with the correct claim rules -- Integrating the Duo ADFS MFA Adapter +- Integrating the Duo ADFS MFA Adapter (optional) - Firewall rules between the zones - Connecting ADFS to NetBird as a generic OIDC provider @@ -72,7 +72,7 @@ This topology follows Microsoft's recommended deployment for ADFS: | Server | Network Zone | Domain Joined | Roles | | --- | --- | --- | --- | | Domain Controller | Corporate LAN | Yes (DC) | AD DS, DNS | -| ADFS Server | Corporate LAN | Yes (member) | ADFS, Duo ADFS Adapter | +| ADFS Server | Corporate LAN | Yes (member) | ADFS, Duo ADFS Adapter (if using Duo MFA) | | Web Application Proxy | DMZ | Optional (recommended: no) | Remote Access (WAP role) | | NetBird Management | DMZ | No | NetBird management, signal, relay, STUN | @@ -107,7 +107,7 @@ No other traffic from the DMZ should reach the corporate LAN. This is the critic | ADFS Server | Domain Controller | 389/636 | TCP | LDAP/LDAPS for authentication | | ADFS Server | Domain Controller | 88 | TCP/UDP | Kerberos | | ADFS Server | Domain Controller | 53 | TCP/UDP | DNS | -| ADFS Server | Duo Cloud (`*.duosecurity.com`) | 443 | TCP | Duo MFA verification (outbound) | +| ADFS Server | Duo Cloud (`*.duosecurity.com`) | 443 | TCP | Duo MFA verification (outbound, only if using Duo MFA) | --- @@ -117,7 +117,7 @@ No other traffic from the DMZ should reach the corporate LAN. This is the critic - Windows Server 2016 or later for the WAP server (can be a different version than the ADFS server) - A functioning Active Directory domain with user accounts - A TLS certificate for the ADFS service name (e.g., `adfs.example.com`), issued by a CA trusted by both internal and external clients. The same certificate must be installed on both the ADFS server and the WAP server. -- A Cisco Duo account (Essentials, Advantage, or Premier plan) +- A Cisco Duo account (Essentials, Advantage, or Premier plan) — only required if you plan to enforce Duo MFA in [Step 6](#step-6-install-and-configure-the-duo-adfs-mfa-adapter) - A deployed NetBird self-hosted instance - DNS configured so that: - External DNS resolves the ADFS service name to the WAP's public IP (or DMZ load balancer) @@ -172,58 +172,143 @@ Changing UPNs in a production environment can affect other services that depend ## Step 1: Install and Configure ADFS on the Member Server -### 1.1 Install the ADFS Role +This step installs the ADFS role on a dedicated Windows Server, creates a service account for ADFS to run as, and configures the federation farm. At the end, you'll have a working ADFS server that's reachable on the internal network. Exposing it externally is handled by the WAP in [Step 5](#step-5-install-and-configure-web-application-proxy). + +Before starting, make sure you've already provisioned the ADFS server itself. It should be: + +- A Windows Server 2016 or later VM (Windows Server 2022 or 2025 recommended) +- Sized at minimum 2 vCPU, 8 GB RAM, 80 GB disk +- Joined to your Active Directory domain (this is required; unlike WAP, ADFS must be domain-joined) +- Placed on the internal corporate LAN, not in the DMZ +- Not the domain controller itself; ADFS requires a separate server -On the dedicated ADFS member server (not the domain controller): +All commands below run on the ADFS server in an elevated PowerShell session unless otherwise noted. + +### 1.1 Install the ADFS Role ```powershell Install-WindowsFeature ADFS-Federation -IncludeManagementTools ``` +This installs the ADFS role binaries and the AD FS Management console. The role is installed but not yet configured; no services are started at this point. The command takes 1–2 minutes and does not require a reboot. + +Verify the installation completed: + +```powershell +Get-WindowsFeature ADFS-Federation +``` + +The `InstallState` column should show `Installed`. + ### 1.2 Install the TLS Certificate -Import your CA-issued TLS certificate into the Local Computer Personal store (`Cert:\LocalMachine\My`). Note the thumbprint. +ADFS requires a TLS certificate whose subject or SAN matches your federation service name (e.g., `adfs.example.com`). This certificate is used for: + +- The HTTPS endpoint that browsers connect to for authentication +- The TLS binding that WAP uses when proxying traffic to ADFS +- Token signing and encryption (ADFS can use self-signed certs for these internally, but using the same public cert simplifies management) + +The certificate must be issued by a CA that both internal and external clients trust. For production, use a public CA (Let's Encrypt, DigiCert, Sectigo, etc.) so external users don't get certificate warnings. The certificate must include the private key. -If the certificate was issued as a PFX file: +**Importing a PFX file (most common):** + +If your certificate was provided as a PFX (or PFX-renamed-to-P12) file with a password: ```powershell $pfxPassword = ConvertTo-SecureString "your-pfx-password" -AsPlainText -Force + Import-PfxCertificate -FilePath "C:\path\to\adfs.pfx" ` -CertStoreLocation "Cert:\LocalMachine\My" ` -Password $pfxPassword ``` -Note the thumbprint: +The certificate is imported into the Local Computer's Personal certificate store. This is where ADFS and WAP both look for certificates. + +**Get the thumbprint for later use:** ```powershell Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Subject -like "*adfs*" } | Format-List Subject, Thumbprint, NotAfter ``` +Copy the thumbprint value. You'll paste it into the ADFS farm installation command in [Step 1.4](#14-configure-the-adfs-farm), and you'll need it again in [Step 5](#step-5-install-and-configure-web-application-proxy) when configuring WAP. + +Also note the `NotAfter` date. Set a calendar reminder 30 days before this date so you can renew the certificate before it expires. An expired cert breaks all authentication. + +**Verify the certificate works:** + +```powershell +Test-Certificate -Cert (Get-ChildItem Cert:\LocalMachine\My\) -Policy SSL +``` + +This returns `True` if the certificate chains successfully to a trusted root CA. If it returns `False`, your CA's intermediate or root certificates may not be installed on this server. + ### 1.3 Create a Group Managed Service Account (gMSA) -Microsoft recommends using a gMSA for the ADFS service account in production deployments: +ADFS needs an identity to run its services under. You have three options: + +1. **Group Managed Service Account (gMSA)** — recommended for production. Password is managed automatically by AD, rotated every 30 days, never known to any human. Can be used across multiple servers in a farm. +2. **Standard service account** — a regular AD user account with a manually set password. Works but requires manual password management. +3. **Built-in account** — running as LocalSystem or NetworkService. Not recommended; lacks the isolation and auditability of a dedicated account. + +This guide uses gMSA. If your environment's policies require a standard service account instead, the `Install-AdfsFarm` command in [Step 1.4](#14-configure-the-adfs-farm) accepts a `-ServiceAccountCredential` parameter instead of `-GroupServiceAccountIdentifier`. + +**One-time setup on the domain controller (skip if already done):** + +The AD forest needs a KDS root key before it can generate gMSA passwords. Check if one already exists by running this on the domain controller: + +```powershell +Get-KdsRootKey +``` + +If no keys are returned, create one. Run this on the domain controller: ```powershell -# Run on the domain controller first (one-time setup) Add-KdsRootKey -EffectiveImmediately +``` + +The `-EffectiveImmediately` parameter is a misnomer. In production, the key isn't usable until 10 hours after creation (the delay allows the key to replicate across all domain controllers). In a lab environment, you can force it to be immediately usable: -# Then create the gMSA (can be run from the ADFS server) +```powershell +Add-KdsRootKey -EffectiveTime ((Get-Date).AddHours(-10)) +``` + +Use the dated-back version only in labs. For production, run `Add-KdsRootKey -EffectiveImmediately` and wait 10 hours before proceeding. + +**Create the gMSA (run from the ADFS server or any domain-joined server):** + +```powershell New-ADServiceAccount -Name "svc-adfs" ` -DnsHostName "adfs.example.com" ` -PrincipalsAllowedToRetrieveManagedPassword "ADFS-Server$" ``` -Replace `ADFS-Server$` with the computer account name of your ADFS member server. If you have multiple ADFS servers in a farm, use a group containing all of their computer accounts. +Replace values: + +- `svc-adfs`: the service account name. You can choose any name; `svc-adfs` is a common convention. +- `adfs.example.com`: your federation service name (same as the TLS certificate's subject). +- `ADFS-Server$`: the computer account name of the ADFS server. The trailing `$` is important; computer accounts in AD always end with `$`. If your ADFS server's hostname is `ADFS01`, you'd use `ADFS01$` here. + +If you have multiple ADFS servers in a farm, replace the single computer account with a security group containing all the ADFS server computer accounts, then use the group name instead. -Install the gMSA on the ADFS server: +**Install the gMSA on the ADFS server:** + +This step retrieves the gMSA's password from AD and caches it locally on the server, so ADFS can use the account. ```powershell Install-ADServiceAccount -Identity "svc-adfs" -Test-ADServiceAccount -Identity "svc-adfs" # Should return True +Test-ADServiceAccount -Identity "svc-adfs" ``` +`Test-ADServiceAccount` should return `True`. If it returns `False`, the most common causes are: + +- The KDS root key is not yet effective (10 hour wait in production) +- The ADFS server's computer account is not in the `PrincipalsAllowedToRetrieveManagedPassword` list +- The Active Directory module is not available (run `Install-WindowsFeature RSAT-AD-PowerShell`) + ### 1.4 Configure the ADFS Farm +Now you actually configure ADFS to run. This creates the federation service, registers the gMSA, binds the TLS certificate to the HTTPS endpoint, and initializes the configuration database. + ```powershell $certThumbprint = "" @@ -235,24 +320,58 @@ Install-AdfsFarm ` -OverwriteConfiguration ``` -After the command completes, restart the server: +Replace values: + +- ``: the thumbprint from [Step 1.2](#12-install-the-tls-certificate). +- `adfs.example.com`: your federation service name. This must match the TLS certificate and must be DNS-resolvable. +- `Corporate ADFS`: a friendly display name shown to users on the ADFS sign-in page. Customize to your organization. +- `CORP\svc-adfs$`: your NetBIOS domain name followed by the gMSA name, with a trailing `$`. If your domain's NetBIOS name is `CONTOSO`, you'd use `CONTOSO\svc-adfs$`. + +The `-OverwriteConfiguration` flag tells the installer to overwrite any existing ADFS configuration. On a fresh server, this has no effect. If you're rebuilding, it wipes the previous config. + +The command takes 2–4 minutes. Output ends with a summary showing the federation service was configured successfully. + +**Restart the server:** ```powershell Restart-Computer ``` +After the reboot, the ADFS service starts automatically. Wait 2–3 minutes after the server comes back up before running the verification step; ADFS takes a moment to fully initialize. + ### 1.5 Verify ADFS is Running +Check that the ADFS service is running: + ```powershell Get-Service adfssrv +``` + +`Status` should be `Running`. If it's `Stopped`, try starting it: -Invoke-WebRequest -Uri "https://adfs.example.com/adfs/.well-known/openid-configuration" ` - -UseBasicParsing | Select-Object -ExpandProperty Content +```powershell +Start-Service adfssrv +Get-EventLog -LogName "AD FS/Admin" -Newest 20 ``` - -If the ADFS server resolves its own service name to an external IP (for example, through split DNS or public DNS), you may need to add a hosts file entry pointing the ADFS FQDN to `127.0.0.1` and flush DNS (`ipconfig /flushdns`) for local testing to work. - +If the service won't start, review the event log for errors. Common startup issues include certificate problems (revoked, expired, wrong key usage) and gMSA permission problems. + +**Test the OIDC discovery endpoint:** + +```powershell +Invoke-WebRequest -Uri "https://adfs.example.com/adfs/.well-known/openid-configuration" -UseBasicParsing | Select-Object -ExpandProperty Content +``` + +This should return a JSON document with fields including `issuer`, `authorization_endpoint`, `token_endpoint`, and `jwks_uri`. If it does, ADFS is running and responding correctly on the internal network. + +**Troubleshooting:** + +- **"Unable to connect to the remote server"**: The service might not be listening yet (wait longer), or the hostname doesn't resolve to this server. Check with `Resolve-DnsName adfs.example.com` and verify it returns this server's IP. +- **"Could not establish trust relationship for the SSL/TLS secure channel"**: The certificate isn't trusted by this server. If you're using an internal CA, import the CA's root certificate into the Trusted Root Certification Authorities store. +- **"The remote name could not be resolved"**: DNS isn't configured. If the ADFS server tries to resolve its own FQDN and that FQDN's public DNS record points to the WAP (which doesn't exist yet in Step 1), local resolution will fail. Add a hosts file entry on the ADFS server pointing `adfs.example.com` to `127.0.0.1` (for example: `127.0.0.1 adfs.example.com`), then flush DNS with `ipconfig /flushdns`. This makes the ADFS server resolve its own name to itself, bypassing public DNS. You'll remove this entry later once WAP is in place and public DNS points correctly. +- **Returns HTML instead of JSON**: You hit an error page, not the OIDC endpoint. Check the URL; it's case-sensitive and `.well-known/openid-configuration` must be exact. + +At the end of this step, you have a functioning ADFS server accepting requests on the internal network. It is not yet reachable from the internet; that's handled by WAP in [Step 5](#step-5-install-and-configure-web-application-proxy). Proceed to [Step 2](#step-2-configure-the-oidc-application-group) to configure the OIDC application group that NetBird will use. --- @@ -435,70 +554,197 @@ Set-AdfsResponseHeaders -CORSTrustedOrigins @("https://") The WAP server sits in the DMZ and proxies ADFS traffic from the internet to the internal ADFS server. External clients never connect directly to ADFS. If the DMZ is compromised, ADFS can revoke the proxy trust and immediately cut off all external access. -### 5.1 Prepare the WAP Server +### 5.1 Provision the WAP Server + +Before starting this step, you need a second Windows Server, separate from the ADFS server. WAP cannot be installed on the same server as ADFS; Microsoft explicitly blocks this. -The WAP server: +**Server specifications:** -- Should be in the DMZ, separated from the corporate network by a firewall -- Can be non-domain-joined for stronger isolation (recommended for high-security environments). If non-domain-joined, it must be able to resolve the ADFS service name to the ADFS server's internal IP. -- Needs the same TLS certificate installed that is used on the ADFS server -- Needs TCP 443 inbound from the internet and TCP 443 outbound to the ADFS server +- Windows Server 2016 or later (Windows Server 2022 or 2025 recommended) +- Minimum 2 vCPU, 4 GB RAM, 60 GB disk (standard Windows Server sizing) +- Placed in your DMZ network segment, not the corporate LAN +- A single network interface is sufficient (the WAP handles inbound from internet and outbound to ADFS on the same NIC, separated by firewall rules) -If non-domain-joined, add a hosts file entry on the WAP server so it can resolve the ADFS service name: +**Domain join decision:** +- **Non-domain-joined (recommended for high-security environments):** Stronger isolation. If the WAP is compromised, the attacker has no AD credentials to pivot with. This is the Microsoft-recommended configuration for internet-facing WAP deployments. +- **Domain-joined:** Simpler to manage (Kerberos, group policy, centralized auth for admins) but increases blast radius if compromised. Only use this in lower-sensitivity environments. + +The rest of this guide assumes non-domain-joined. If you choose domain-joined, the installation commands are the same; only the name resolution step below differs. + +**Network requirements (firewall rules that must exist before you proceed):** + +| Direction | Source | Destination | Port | Purpose | +| --- | --- | --- | --- | --- | +| Inbound | Internet | WAP | TCP 443 | External authentication requests | +| Outbound | WAP | ADFS server (internal IP) | TCP 443 | Proxied requests to ADFS | +| Outbound | WAP | Public DNS or internal DNS | UDP 53 | Name resolution | + +No other traffic should be permitted from the DMZ to the corporate LAN. If the DMZ firewall allows broader access, tighten it before proceeding. + +**Name resolution setup (non-domain-joined WAP):** + +Since the WAP is not domain-joined, it does not use your internal DNS by default. It needs to resolve the ADFS service name (e.g., `adfs.example.com`) to the ADFS server's **internal IP address**, not the WAP's own public IP. + +Open `C:\Windows\System32\drivers\etc\hosts` in Notepad (run as Administrator) and add a line like ` adfs.example.com`, replacing `` with the actual internal IP of your ADFS server (for example, `10.0.1.50`). Save the file. + +Test the name resolution from PowerShell on the WAP: + +```powershell +ping adfs.example.com +``` + +This should resolve to the internal IP and (if the firewall rule from the table above is in place) should also succeed. If ping fails but the IP is correct, ICMP might be blocked by the firewall; that's fine as long as TCP 443 works. Test TCP 443 directly: + +```powershell +Test-NetConnection -ComputerName adfs.example.com -Port 443 +``` + +`TcpTestSucceeded : True` confirms the network path works. + +**Install the TLS certificate:** + +WAP must have the exact same TLS certificate installed that ADFS uses, with the same thumbprint. This is not a second certificate for the same domain; it is literally the same certificate file. Export it from the ADFS server (including the private key) and import it into the WAP server's Local Computer Personal store. + +On the ADFS server, export the cert to a PFX file: + +```powershell +$pfxPassword = ConvertTo-SecureString "choose-a-strong-password" -AsPlainText -Force +Export-PfxCertificate -Cert "Cert:\LocalMachine\My\" ` + -FilePath "C:\temp\adfs-cert.pfx" ` + -Password $pfxPassword +``` + +Transfer the PFX file securely to the WAP server (SMB share, secure copy, etc., not email). On the WAP server, import it: + +```powershell +$pfxPassword = ConvertTo-SecureString "the-password-you-chose" -AsPlainText -Force +Import-PfxCertificate -FilePath "C:\path\to\adfs-cert.pfx" ` + -CertStoreLocation "Cert:\LocalMachine\My" ` + -Password $pfxPassword ``` - adfs.example.com + +After transfer, delete the PFX file from both servers. It contains the private key and should not be left on disk. + +Verify the thumbprint matches on both servers: + +```powershell +Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Subject -like "*adfs*" } | Select-Object Subject, Thumbprint ``` -Import the TLS certificate into the WAP server's Personal store, the same way as on the ADFS server ([Step 1.2](#12-install-the-tls-certificate)). +If the thumbprints differ, WAP will fail to establish the proxy trust in [Step 5.3](#53-establish-the-proxy-trust-with-adfs). ### 5.2 Install the WAP Role +On the WAP server, run PowerShell as Administrator: + ```powershell Install-WindowsFeature Web-Application-Proxy -IncludeManagementTools ``` -### 5.3 Configure WAP to Trust ADFS +No reboot is required. This installs the Remote Access role with the Web Application Proxy component and the management console. + +### 5.3 Establish the Proxy Trust with ADFS + +**What this step does:** It creates a cryptographic trust between the WAP server and the ADFS server. After this runs, the ADFS server recognizes the WAP as an authorized proxy and will accept forwarded authentication requests from it. You only run this once; the trust persists until you explicitly revoke it. + +**What you need before running this:** + +- The ADFS server must be running and reachable from the WAP on TCP 443 (test with the `Test-NetConnection` command from [Step 5.1](#51-provision-the-wap-server)) +- The TLS certificate must be installed on the WAP with the same thumbprint as on ADFS +- You need domain credentials that have **local administrator rights on the ADFS server**. These are used once during this command to authenticate to the ADFS server and create the trust relationship. They are not stored; after the command completes, the WAP uses a certificate-based trust, not these credentials. + +**Get the certificate thumbprint:** + +On the WAP server, run: + +```powershell +Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Subject -like "*adfs*" } | Select-Object Thumbprint +``` + +Copy the thumbprint value. + +**Run the installation command:** ```powershell $adfsCredential = Get-Credential -# Enter domain credentials that have local admin rights on the ADFS server. -# These are used once to establish the proxy trust and are not stored. +``` + +A Windows credential dialog opens. Enter the domain account in `DOMAIN\username` format (for example, `CORP\adfs-admin`) and its password. Press OK. + +Then run the actual WAP configuration: +```powershell Install-WebApplicationProxy ` - -CertificateThumbprint "" ` + -CertificateThumbprint "" ` -FederationServiceName "adfs.example.com" ` -FederationServiceTrustCredential $adfsCredential ``` +Replace `` with the thumbprint you copied, and `adfs.example.com` with your actual ADFS service FQDN. + +The command takes 30–60 seconds. It will: + +1. Open a TLS connection to `adfs.example.com` on TCP 443 (which resolves to the internal ADFS IP via your hosts file) +2. Authenticate to the ADFS server using the credentials you provided +3. Register the WAP server with ADFS as a trusted proxy +4. Install a machine certificate on the WAP that ADFS will recognize for future trust operations +5. Start the `appproxysvc` and `adfssrv` related services + +**Expected successful output:** + +``` +PublishedApplicationName : Device Registration Service +ADFSUrl : https://adfs.example.com/ +``` + +If you see errors, the most common causes are: + +- Firewall blocking TCP 443 from WAP to ADFS (test with `Test-NetConnection`) +- Name resolution issue: `adfs.example.com` resolves to the wrong IP (check the hosts file) +- Wrong thumbprint (certificate not installed, or a different certificate with a different thumbprint) +- Credentials lack local admin rights on the ADFS server +- ADFS service not running on the ADFS server + ### 5.4 Verify the Proxy Trust -From the WAP server: +On the WAP server: ```powershell Get-WebApplicationProxyHealth ``` -All components should show as healthy. If not, verify: +Every component in the output should show `State: Healthy`. If any component shows unhealthy, the output includes diagnostic details. Common issues: -- TCP 443 is open from the WAP to the ADFS server through the internal firewall -- The WAP can resolve `adfs.example.com` to the ADFS server's internal IP -- The TLS certificate on the WAP matches the one on the ADFS server +- `ProxyTrustRenewalFailed`: The machine certificate ADFS issued to the WAP is expiring or expired. Re-run `Install-WebApplicationProxy` to renew. +- `ADFSServerConnectivity`: The WAP can't reach ADFS. Recheck firewall and DNS. +- `CertificateExpired`: The TLS certificate has expired or is about to. Install a new certificate on both ADFS and WAP, then update the WAP configuration. -### 5.5 Test External Access +### 5.5 Test External Access Through the WAP -From an external machine, navigate to: +From a machine outside your corporate network (your laptop on a home or cellular connection works), open a browser and navigate to: ``` https://adfs.example.com/adfs/.well-known/openid-configuration ``` -DNS should resolve to the WAP's public IP. The WAP proxies the request to the internal ADFS server, and you should see the OIDC discovery JSON. +For this to work: + +- Public DNS must resolve `adfs.example.com` to the WAP server's public IP (not the ADFS server's internal IP, which isn't routable from the internet) +- The DMZ firewall must allow inbound TCP 443 from the internet to the WAP + +If successful, you will see the OIDC discovery JSON returned. The connection path is: your browser → internet → DMZ firewall → WAP (TLS termination) → new connection to internal ADFS server → response back through the same path. + +If you see a TLS error, verify the WAP's certificate is the same one as on ADFS. If you see a connection timeout, check the DMZ firewall rule and public DNS. If you see a 502 or 503 from the WAP, the proxy trust is probably broken; run `Get-WebApplicationProxyHealth` to diagnose. --- ## Step 6: Install and Configure the Duo ADFS MFA Adapter + +This step is optional. Skip it if you don't need MFA or plan to enforce it through a different mechanism (e.g., a conditional access policy, a different MFA adapter, or smart-card authentication). + + The Duo adapter is installed on the **ADFS server** (internal network), not on the WAP. ### 6.1 Create the Application in Duo @@ -628,7 +874,7 @@ This request routes through the WAP and should return the OIDC discovery documen 1. Log out of the NetBird Dashboard 2. On the login page, click the **ADFS** button 3. You are redirected to ADFS (through the WAP) -4. Sign in with AD credentials and complete the Duo MFA challenge +4. Sign in with AD credentials (and complete the Duo MFA challenge if you configured it in [Step 6](#step-6-install-and-configure-the-duo-adfs-mfa-adapter)) 5. Confirm you are redirected back to the NetBird Dashboard as the authenticated user 6. Verify the user's display name, email, and group memberships appear correctly @@ -648,14 +894,14 @@ Confirm the response includes `issuer`, `authorization_endpoint`, `token_endpoin ### Test the Duo MFA Flow (through WAP) -Navigate to `https:///adfs/ls/idpinitiatedsignon` from an external machine. Sign in with an AD account and verify that the Duo MFA prompt appears. +If you configured Duo in [Step 6](#step-6-install-and-configure-the-duo-adfs-mfa-adapter), navigate to `https:///adfs/ls/idpinitiatedsignon` from an external machine. Sign in with an AD account and verify that the Duo MFA prompt appears. ### Test the Full NetBird Login 1. Open the NetBird dashboard 2. The login page should redirect to ADFS (through the WAP) 3. Enter AD credentials -4. Complete the Duo MFA challenge +4. Complete the Duo MFA challenge (if Duo was configured) 5. Confirm you are redirected back to the NetBird dashboard as the authenticated user 6. Verify the user's display name, email, and group memberships appear correctly @@ -712,7 +958,7 @@ If the ADFS server resolves its own FQDN to an external IP, loopback traffic may | Web API | Resource | NetBird - Web API | | Claim Rules | Issuance Transform | Send LDAP Attributes, Send Display Name, Query Group Membership, Filter Group Membership, Send UPN, Override sub claim | | CORS | Trusted Origin | `https://` | -| MFA | Additional Authentication | Duo Authentication for AD FS | +| MFA (optional) | Additional Authentication | Duo Authentication for AD FS | ### NetBird Identity Provider Values From 6139b677e7bb6778cdaaf1048b7dac91a3d0740f Mon Sep 17 00:00:00 2001 From: Jack Carter <128555021+SunsetDrifter@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:28:03 +0200 Subject: [PATCH 4/7] docs: fix ADFS intra-page anchor links @sindresorhus/slugify (the project's heading slug generator) splits CamelCase words (NetBird -> net-bird) and inserts hyphens between period-separated digits (2.3 -> 2-3). Update every in-page anchor to match the generated slugs so step links resolve correctly. Also redirect the UPN row in the AD attributes table to Step 3, since the 'Required NetBird Configuration Settings' subsection it used to reference was removed in the CE rewrite. --- .../selfhosted/identity-providers/adfs.mdx | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/pages/selfhosted/identity-providers/adfs.mdx b/src/pages/selfhosted/identity-providers/adfs.mdx index daf3da9f..31db8cce 100644 --- a/src/pages/selfhosted/identity-providers/adfs.mdx +++ b/src/pages/selfhosted/identity-providers/adfs.mdx @@ -141,7 +141,7 @@ ADFS will pull user attributes from Active Directory and include them as claims | `displayName` | `name` | Display name in the dashboard | | `givenName` | `given_name` | First name | | `sn` | `family_name` | Last name | -| `userPrincipalName` | `upn` | User identifier (see [Step 7.2](#72-required-netbird-configuration-settings)) | +| `userPrincipalName` | `upn` | User identifier (see [Step 3](#step-3-configure-claim-transform-rules)) | Verify that your user objects have these attributes populated: @@ -230,7 +230,7 @@ The certificate is imported into the Local Computer's Personal certificate store Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Subject -like "*adfs*" } | Format-List Subject, Thumbprint, NotAfter ``` -Copy the thumbprint value. You'll paste it into the ADFS farm installation command in [Step 1.4](#14-configure-the-adfs-farm), and you'll need it again in [Step 5](#step-5-install-and-configure-web-application-proxy) when configuring WAP. +Copy the thumbprint value. You'll paste it into the ADFS farm installation command in [Step 1.4](#1-4-configure-the-adfs-farm), and you'll need it again in [Step 5](#step-5-install-and-configure-web-application-proxy) when configuring WAP. Also note the `NotAfter` date. Set a calendar reminder 30 days before this date so you can renew the certificate before it expires. An expired cert breaks all authentication. @@ -250,7 +250,7 @@ ADFS needs an identity to run its services under. You have three options: 2. **Standard service account** — a regular AD user account with a manually set password. Works but requires manual password management. 3. **Built-in account** — running as LocalSystem or NetworkService. Not recommended; lacks the isolation and auditability of a dedicated account. -This guide uses gMSA. If your environment's policies require a standard service account instead, the `Install-AdfsFarm` command in [Step 1.4](#14-configure-the-adfs-farm) accepts a `-ServiceAccountCredential` parameter instead of `-GroupServiceAccountIdentifier`. +This guide uses gMSA. If your environment's policies require a standard service account instead, the `Install-AdfsFarm` command in [Step 1.4](#1-4-configure-the-adfs-farm) accepts a `-ServiceAccountCredential` parameter instead of `-GroupServiceAccountIdentifier`. **One-time setup on the domain controller (skip if already done):** @@ -322,7 +322,7 @@ Install-AdfsFarm ` Replace values: -- ``: the thumbprint from [Step 1.2](#12-install-the-tls-certificate). +- ``: the thumbprint from [Step 1.2](#1-2-install-the-tls-certificate). - `adfs.example.com`: your federation service name. This must match the TLS certificate and must be DNS-resolvable. - `Corporate ADFS`: a friendly display name shown to users on the ADFS sign-in page. Customize to your organization. - `CORP\svc-adfs$`: your NetBIOS domain name followed by the gMSA name, with a trailing `$`. If your domain's NetBIOS name is `CONTOSO`, you'd use `CONTOSO\svc-adfs$`. @@ -405,11 +405,11 @@ New-AdfsApplicationGroup -Name "NetBird" -ApplicationGroupIdentifier "NetBird" | Field | Value | | --- | --- | | Name | `ADFS` (or your preferred display name) | - | Client ID | The GUID from [Step 2.1](#21-generate-a-client-id) | - | Client Secret | Enter a placeholder (you will replace it in [Step 7](#step-7-complete-netbird-configuration)) | + | Client ID | The GUID from [Step 2.1](#2-1-generate-a-client-id) | + | Client Secret | Enter a placeholder (you will replace it in [Step 7](#step-7-complete-net-bird-configuration)) | | Issuer | `https:///adfs` | -6. NetBird displays a **Redirect URL** — copy this value. Do **not** click **Save** yet; you'll return to this tab in [Step 7](#step-7-complete-netbird-configuration). +6. NetBird displays a **Redirect URL** — copy this value. Do **not** click **Save** yet; you'll return to this tab in [Step 7](#step-7-complete-net-bird-configuration). ### 2.4 Add a Server Application (Confidential Client) @@ -427,7 +427,7 @@ $clientSecret = $serverApp.ClientSecret Write-Host "Client Secret: $clientSecret" ``` -Replace `` with the URL you copied in [Step 2.3](#23-get-the-redirect-url-from-netbird). Record the client secret — you'll paste it into NetBird in [Step 7](#step-7-complete-netbird-configuration). ADFS does not display the secret again, so if you lose it you'll need to regenerate it with `Set-AdfsServerApplication ... -GenerateClientSecret`. +Replace `` with the URL you copied in [Step 2.3](#2-3-get-the-redirect-url-from-net-bird). Record the client secret — you'll paste it into NetBird in [Step 7](#step-7-complete-net-bird-configuration). ADFS does not display the secret again, so if you lose it you'll need to regenerate it with `Set-AdfsServerApplication ... -GenerateClientSecret`. ### 2.5 Add a Web API @@ -632,7 +632,7 @@ Verify the thumbprint matches on both servers: Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Subject -like "*adfs*" } | Select-Object Subject, Thumbprint ``` -If the thumbprints differ, WAP will fail to establish the proxy trust in [Step 5.3](#53-establish-the-proxy-trust-with-adfs). +If the thumbprints differ, WAP will fail to establish the proxy trust in [Step 5.3](#5-3-establish-the-proxy-trust-with-adfs). ### 5.2 Install the WAP Role @@ -650,7 +650,7 @@ No reboot is required. This installs the Remote Access role with the Web Applica **What you need before running this:** -- The ADFS server must be running and reachable from the WAP on TCP 443 (test with the `Test-NetConnection` command from [Step 5.1](#51-provision-the-wap-server)) +- The ADFS server must be running and reachable from the WAP on TCP 443 (test with the `Test-NetConnection` command from [Step 5.1](#5-1-provision-the-wap-server)) - The TLS certificate must be installed on the WAP with the same thumbprint as on ADFS - You need domain credentials that have **local administrator rights on the ADFS server**. These are used once during this command to authenticate to the ADFS server and create the trust relationship. They are not stored; after the command completes, the WAP uses a certificate-based trust, not these credentials. @@ -812,17 +812,17 @@ From an external machine, navigate to `https://adfs.example.com/adfs/ls/idpiniti ### 7.1 Finish Adding the Identity Provider -Return to the NetBird Dashboard tab you opened in [Step 2.3](#23-get-the-redirect-url-from-netbird): +Return to the NetBird Dashboard tab you opened in [Step 2.3](#2-3-get-the-redirect-url-from-net-bird): -1. Replace the placeholder **Client Secret** field with the secret generated in [Step 2.4](#24-add-a-server-application-confidential-client) +1. Replace the placeholder **Client Secret** field with the secret generated in [Step 2.4](#2-4-add-a-server-application-confidential-client) 2. Verify the other values: | Field | Value | | --- | --- | | Type | Generic OIDC | | Name | `ADFS` (or your chosen display name) | - | Client ID | The GUID from [Step 2.1](#21-generate-a-client-id) | - | Client Secret | From [Step 2.4](#24-add-a-server-application-confidential-client) | + | Client ID | The GUID from [Step 2.1](#2-1-generate-a-client-id) | + | Client Secret | From [Step 2.4](#2-4-add-a-server-application-confidential-client) | | Issuer | `https:///adfs` | 3. Click **Save** @@ -911,7 +911,7 @@ If you configured Duo in [Step 6](#step-6-install-and-configure-the-duo-adfs-mfa ### Login returns a 400 error on token exchange -The ADFS application is most likely configured as a Native Application (public client) instead of a Server Application (confidential client). NetBird's Dashboard IdP flow sends a `client_secret` during token exchange, which ADFS will reject if the app was created with `Add-AdfsNativeClientApplication`. Delete the Native Application and recreate it using `Add-AdfsServerApplication` per [Step 2.4](#24-add-a-server-application-confidential-client), then re-grant permissions per [Step 2.6](#26-grant-oidc-permissions). Also verify the client secret pasted into NetBird matches the one generated in ADFS. +The ADFS application is most likely configured as a Native Application (public client) instead of a Server Application (confidential client). NetBird's Dashboard IdP flow sends a `client_secret` during token exchange, which ADFS will reject if the app was created with `Add-AdfsNativeClientApplication`. Delete the Native Application and recreate it using `Add-AdfsServerApplication` per [Step 2.4](#2-4-add-a-server-application-confidential-client), then re-grant permissions per [Step 2.6](#2-6-grant-oidc-permissions). Also verify the client secret pasted into NetBird matches the one generated in ADFS. ### Dashboard login silently fails with no error @@ -969,9 +969,9 @@ Entered in **Settings > Identity Providers > Add Identity Provider** in the NetB | Type | Generic OIDC | | Name | `ADFS` (display name on the login page) | | Issuer | `https:///adfs` | -| Client ID | The GUID generated in [Step 2.1](#21-generate-a-client-id) | -| Client Secret | Generated by `Add-AdfsServerApplication` in [Step 2.4](#24-add-a-server-application-confidential-client) | -| Redirect URL | Generated by NetBird; registered in ADFS in [Step 2.4](#24-add-a-server-application-confidential-client) | +| Client ID | The GUID generated in [Step 2.1](#2-1-generate-a-client-id) | +| Client Secret | Generated by `Add-AdfsServerApplication` in [Step 2.4](#2-4-add-a-server-application-confidential-client) | +| Redirect URL | Generated by NetBird; registered in ADFS in [Step 2.4](#2-4-add-a-server-application-confidential-client) | --- From 5df5f8b600a43006c9931cc0a1015e2b52c540b2 Mon Sep 17 00:00:00 2001 From: Jack Carter <128555021+SunsetDrifter@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:35:32 +0200 Subject: [PATCH 5/7] docs: note that ADFS group-membership claim rules are optional Rules 3a and 3b in Step 3 produce the 'groups' claim consumed by JWT Group Sync. Add a Note explaining they can be skipped if group sync isn't needed, and clarify that 3a and 3b must be kept together (3a emits into a temp claim, 3b filters and renames it to 'groups'). --- src/pages/selfhosted/identity-providers/adfs.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/selfhosted/identity-providers/adfs.mdx b/src/pages/selfhosted/identity-providers/adfs.mdx index 31db8cce..485aea2a 100644 --- a/src/pages/selfhosted/identity-providers/adfs.mdx +++ b/src/pages/selfhosted/identity-providers/adfs.mdx @@ -454,6 +454,10 @@ Grant-AdfsApplicationPermission ` NetBird requires specific claims in the OIDC tokens. Add these issuance transform rules to the Web API: + +Rules **3a** and **3b** are only needed if you plan to enable [JWT Group Sync](#7-3-enable-jwt-group-sync) so NetBird can mirror AD group memberships. If you don't need group sync, skip both rules and omit `$groupQueryRule + $groupFilterRule` from the `Set-AdfsWebApiApplication` call at the bottom of this step. Rule 3b alone is not useful without 3a — 3a emits groups into the temporary claim type `http://temp/groups`, and 3b filters them and re-emits as the `groups` claim NetBird actually reads. + + ```powershell # Rule 1: Send user attributes (email, first name, last name) $ldapRule = @" From 0131a06853d89a1df84c9dbf0b137fa9d5536426 Mon Sep 17 00:00:00 2001 From: Jack Carter <128555021+SunsetDrifter@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:38:47 +0200 Subject: [PATCH 6/7] docs: expand ADFS Step 3 intro with context and per-rule overview The prior one-sentence intro ('NetBird requires specific claims in the OIDC tokens') didn't explain what issuance transform rules are or what each of the six rules does. Add a paragraph on why ADFS needs them and a short bullet list describing each rule's purpose and dependencies (e.g., Rule 5 depends on Rule 4). The optional-rules Note and code block follow unchanged. --- src/pages/selfhosted/identity-providers/adfs.mdx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/pages/selfhosted/identity-providers/adfs.mdx b/src/pages/selfhosted/identity-providers/adfs.mdx index 485aea2a..2705ad32 100644 --- a/src/pages/selfhosted/identity-providers/adfs.mdx +++ b/src/pages/selfhosted/identity-providers/adfs.mdx @@ -452,12 +452,23 @@ Grant-AdfsApplicationPermission ` ## Step 3: Configure Claim Transform Rules -NetBird requires specific claims in the OIDC tokens. Add these issuance transform rules to the Web API: +By default, the OIDC tokens ADFS issues only include a user's Windows account name. NetBird needs more than that — email, display name, first and last name, a URL-safe user identifier, and optionally AD group memberships — to populate the dashboard and evaluate access policies. You fill that gap with **issuance transform rules**: claim-language snippets attached to the Web API that tell ADFS which AD attributes to look up for each authenticating user and which OIDC claims to emit them as. + +Here's what each rule does: + +- **Rule 1 — LDAP attributes:** emits `email`, `given_name`, and `family_name` from the AD `mail`, `givenName`, and `sn` attributes. +- **Rule 2 — Display name:** emits the `name` claim from the AD `displayName` attribute. Kept separate from Rule 1 to avoid a duplicate-`name` issue that would break NetBird's user display (see the first Note after the code block). +- **Rule 3a — Group query:** reads every AD group the user belongs to into a temporary claim type. +- **Rule 3b — Group filter:** narrows the temporary claim to groups matching your naming convention and re-emits them as the `groups` claim NetBird consumes. +- **Rule 4 — UPN:** emits the `upn` claim from the AD `userPrincipalName` attribute. Rule 5 depends on this. +- **Rule 5 — `sub` override:** replaces ADFS's default base64-encoded pairwise `sub` with the UPN so the user identifier is URL-safe (see the second Note after the code block). Rules **3a** and **3b** are only needed if you plan to enable [JWT Group Sync](#7-3-enable-jwt-group-sync) so NetBird can mirror AD group memberships. If you don't need group sync, skip both rules and omit `$groupQueryRule + $groupFilterRule` from the `Set-AdfsWebApiApplication` call at the bottom of this step. Rule 3b alone is not useful without 3a — 3a emits groups into the temporary claim type `http://temp/groups`, and 3b filters them and re-emits as the `groups` claim NetBird actually reads. +Add these issuance transform rules to the Web API: + ```powershell # Rule 1: Send user attributes (email, first name, last name) $ldapRule = @" From 03398582f899df696df41185b462fc1741a108a9 Mon Sep 17 00:00:00 2001 From: Jack Carter <128555021+SunsetDrifter@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:56:52 +0200 Subject: [PATCH 7/7] docs: fix ADFS guide inaccuracies flagged in review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Get-EventLog with Get-WinEvent in Step 1.5 — Get-EventLog only reads classic logs and cannot open 'AD FS/Admin', which lives under Applications and Services Logs. - Remove references to Set-AdfsServerApplication -PairwiseIdentifierEnabled $false; that parameter does not exist on the cmdlet. Replace the fallback guidance with NETBIRD_AUTH_USER_ID_CLAIM="upn" in setup.env, which was the actual POC fix alongside the Rule 5 claim override. - Restructure the 404 troubleshooting entry as a two-step fix (claim rule + NetBird env var) with a decode-token sanity check. - Drop the 'Domain Users' example from the JWT group sync paragraph since Rule 3b's default '^NetBird-' filter would exclude it; clarify that visible groups are governed by the filter regex. - Relabel the LDAP/LDAPS firewall row as 'directory and attribute lookups (claim data)' rather than 'authentication'; ADFS authenticates users via Kerberos and uses LDAP for attribute lookup. - Add a clarifying Note to Step 2.5 explaining that the guide reuses the client_id as the Web API identifier for simplicity, and larger environments may prefer a distinct resource URI. --- .../selfhosted/identity-providers/adfs.mdx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/pages/selfhosted/identity-providers/adfs.mdx b/src/pages/selfhosted/identity-providers/adfs.mdx index 2705ad32..b939aaf0 100644 --- a/src/pages/selfhosted/identity-providers/adfs.mdx +++ b/src/pages/selfhosted/identity-providers/adfs.mdx @@ -104,7 +104,7 @@ No other traffic from the DMZ should reach the corporate LAN. This is the critic | Source | Destination | Port | Protocol | Purpose | | --- | --- | --- | --- | --- | -| ADFS Server | Domain Controller | 389/636 | TCP | LDAP/LDAPS for authentication | +| ADFS Server | Domain Controller | 389/636 | TCP | LDAP/LDAPS for directory and attribute lookups (claim data) | | ADFS Server | Domain Controller | 88 | TCP/UDP | Kerberos | | ADFS Server | Domain Controller | 53 | TCP/UDP | DNS | | ADFS Server | Duo Cloud (`*.duosecurity.com`) | 443 | TCP | Duo MFA verification (outbound, only if using Duo MFA) | @@ -351,7 +351,7 @@ Get-Service adfssrv ```powershell Start-Service adfssrv -Get-EventLog -LogName "AD FS/Admin" -Newest 20 +Get-WinEvent -LogName "AD FS/Admin" -MaxEvents 20 ``` If the service won't start, review the event log for errors. Common startup issues include certificate problems (revoked, expired, wrong key usage) and gMSA permission problems. @@ -431,6 +431,10 @@ Replace `` with the URL you copied in [Step 2.3](#2-3 ### 2.5 Add a Web API + +This guide uses the same identifier (`$clientId`) for the Server Application and the Web API for simplicity. `Grant-AdfsApplicationPermission` in [Step 2.6](#2-6-grant-oidc-permissions) links them explicitly. In larger environments you may see the Web API use a distinct resource URI like `api://netbird`; both patterns are valid. + + ```powershell Add-AdfsWebApiApplication ` -Name "NetBird - Web API" ` @@ -547,7 +551,7 @@ The `name` claim must be emitted in its own rule (Rule 2) using the short claim -**Why the sub override (Rule 5) is required:** ADFS generates base64-encoded pairwise subject identifiers for the `sub` claim by default. Base64 encoding can produce `/`, `+`, and `=` characters. When NetBird uses the `sub` claim as the user identifier in API URL paths, the `/` characters are interpreted as path separators, causing 404 errors. Emitting `sub` from the UPN (Rule 5) provides a URL-safe, human-readable identifier instead. On some ADFS builds you may also need to disable pairwise identifiers on the Server Application (`Set-AdfsServerApplication ... -PairwiseIdentifierEnabled $false`) — check Microsoft's [AD FS OpenID Connect documentation](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-openid-connect-oauth-flows-scenarios) for your version. +**Why the sub override (Rule 5) is required:** ADFS generates base64-encoded pairwise subject identifiers for the `sub` claim by default. Base64 encoding can produce `/`, `+`, and `=` characters. When NetBird uses the `sub` claim as the user identifier in API URL paths, the `/` characters are interpreted as path separators, causing 404 errors. Emitting `sub` from the UPN (Rule 5) provides a URL-safe, human-readable identifier instead. As an additional safeguard, set `NETBIRD_AUTH_USER_ID_CLAIM="upn"` in NetBird's `setup.env` so NetBird uses the UPN claim directly rather than relying on the overridden `sub`. Groups are filtered at the ADFS level so only relevant groups appear in the JWT and get synced into NetBird. @@ -866,7 +870,7 @@ To synchronize AD group memberships into NetBird for use in access control polic 2. Toggle **Enable JWT group sync** 3. Set the JWT claim name to `groups` -Groups appear in NetBird using the short names from the token (for example, `NetBird-Users`, `Domain Users`). Groups are synced at login time — when a user authenticates, their current group memberships are read from the token. Only groups matching the ADFS filter pattern will appear in NetBird, and that filter is controlled by the regex in the "Filter Group Membership" claim rule. +Groups appear in NetBird using the short names from the token (for example, `NetBird-Users`), filtered by the regex in your "Filter Group Membership" claim rule. Only groups matching the filter will appear in NetBird. Groups are synced at login time — when a user authenticates, their current group memberships are read from the token. Groups with matching names in NetBird and ADFS will **not** sync. To import a group from ADFS, first delete the existing group with that name in NetBird. @@ -946,7 +950,12 @@ The ADFS `name` claim contains two values: one from the implicit Windows account ### NetBird API returns 404 for user operations -The ADFS `sub` claim contains base64 characters (`/`, `+`, `=`) that break URL path routing. Verify that the "Override sub claim" rule (Rule 5 in [Step 3](#step-3-configure-claim-transform-rules)) is active on the Web API. On some ADFS versions the default pairwise identifier is still emitted alongside your override; in that case disable pairwise identifiers on the Server Application with `Set-AdfsServerApplication ... -PairwiseIdentifierEnabled $false`. +The ADFS `sub` claim contains base64 characters (`/`, `+`, `=`) that break URL path routing. Two fixes work together: + +1. Verify that the "Override sub claim" rule (Rule 5 in [Step 3](#step-3-configure-claim-transform-rules)) is active on the Web API. +2. Set `NETBIRD_AUTH_USER_ID_CLAIM="upn"` in NetBird's `setup.env` so NetBird uses the UPN claim directly. + +If both are in place and the error persists, decode a fresh id\_token and confirm the `sub` value is now the UPN (for example, `alice@example.com`) rather than a base64 string. ### Redirect URI mismatch errors