diff --git a/src/components/NavigationDocs.jsx b/src/components/NavigationDocs.jsx index 16b4de2e..0f83f8fd 100644 --- a/src/components/NavigationDocs.jsx +++ b/src/components/NavigationDocs.jsx @@ -631,6 +631,10 @@ export const docsNavigation = [ title: 'Enable Reverse Proxy', href: '/selfhosted/migration/enable-reverse-proxy', }, + { + title: 'External IdP to Embedded IdP', + href: '/selfhosted/migration/external-to-embedded-idp', + }, ], }, ], diff --git a/src/pages/selfhosted/migration/combined-container.mdx b/src/pages/selfhosted/migration/combined-container.mdx index 6295627c..e5d21cd3 100644 --- a/src/pages/selfhosted/migration/combined-container.mdx +++ b/src/pages/selfhosted/migration/combined-container.mdx @@ -14,7 +14,7 @@ This guide walks you through migrating a pre-v0.65.0 NetBird self-hosted deploym -The migration script **exits with an error** if it detects an external identity provider (Auth0, Keycloak, Okta, Zitadel, Google Workspace, Microsoft Entra ID, etc.). If you are using an external IdP, do not run this script. Instead, follow the [self-hosting quickstart](/selfhosted/selfhosted-quickstart) for a fresh installation. +The migration script **exits with an error** if it detects an external identity provider (Auth0, Keycloak, Okta, Zitadel, Google Workspace, Microsoft Entra ID, etc.). If you are using an external IdP, first follow the [External IdP to Embedded IdP migration guide](/selfhosted/migration/external-to-embedded-idp) to switch to the embedded Dex IdP, then return here to complete the combined container migration. ## Overview of changes diff --git a/src/pages/selfhosted/migration/external-to-embedded-idp.mdx b/src/pages/selfhosted/migration/external-to-embedded-idp.mdx new file mode 100644 index 00000000..1685411b --- /dev/null +++ b/src/pages/selfhosted/migration/external-to-embedded-idp.mdx @@ -0,0 +1,482 @@ +import {Note, Warning, Success} from "@/components/mdx" + +export const description = 'Migrate a self-hosted NetBird deployment from an external identity provider to the embedded Dex-based IdP introduced in v0.60.0.' + +# Migration Guide: External IdP to Embedded IdP + +This guide walks through migrating a self-hosted NetBird deployment from an external identity provider to the embedded Dex-based IdP introduced in v0.60.0. + + +**Who is this guide for?** This migration guide is for users who: +- Have an existing self-hosted deployment using an **external IdP** (Auth0, Azure AD, Keycloak, Okta, Authentik, PocketID, Google, Zitadel, or JumpCloud) +- Want to move to the **embedded Dex-based IdP** for a simpler, self-contained authentication setup + + +## Overview + +The migration tool does two things: + + +Migrating to the embedded IdP also unlocks the [Combined Container Setup migration](/selfhosted/migration/combined-container), which consolidates management, signal, relay, and STUN into a single container. If you plan to simplify your deployment, complete this IdP migration first, then follow the combined container guide. + + + +1. **Re-encodes user IDs** in the database to include the external connector ID, so Dex can route returning users to the correct external provider. +2. **Generates a new `management.json`** that replaces `IdpManagerConfig` with `EmbeddedIdP` and updates OAuth2 endpoints to the embedded Dex issuer. + +After migration, existing users keep logging in through the same external provider — Dex acts as a broker in front of it. No passwords or credentials change. + +--- + +## Before You Begin + +### Prerequisites + +| Requirement | Details | +|-------------|---------| +| NetBird version | v0.66.4 or later | +| Config access | You can read and write `management.json` | +| Server downtime | The management server **must be stopped** during migration | +| Backups | Back up your database and config before starting | + +### Supported Providers + +| Provider | Auto-detected | Connector type | Extra setup needed? | +|----------|:---:|----------------|---------------------| +| Auth0 | ✅ | Generic OIDC | No | +| Azure AD | ✅ | Entra | No | +| Keycloak | ✅ | Keycloak | No | +| Okta | ✅ | OIDC | No | +| Authentik | ✅ | OIDC | No | +| PocketID | ✅ | OIDC | No | +| Google | ✅ | Google | No | +| Zitadel | ❌ | Zitadel | Yes — see [Step 2](#step-2-prepare-your-provider-if-required) | +| JumpCloud | ❌ | — | No Dex connector; manual OIDC setup required | + + +**Which path do I follow?** + +- **Auto-detected provider** → Skip Step 2 entirely. The tool reads your `management.json` and builds the connector automatically. +- **Zitadel** → You must complete Step 2 to create an OAuth app and supply connector credentials. +- **JumpCloud or other unsupported provider** → You must complete Step 2 to provide a custom OIDC connector. + + +--- + +## Step 1: Get the Migration Tool + +**Option A — Download a pre-built binary:** + +```bash +# Replace VERSION with the release tag, and adjust the architecture as needed +curl -L -o netbird-idp-migrate.tar.gz \ + https://github.com/netbirdio/netbird/releases/download/VERSION/netbird-idp-migrate_VERSION_linux_amd64.tar.gz +tar xzf netbird-idp-migrate.tar.gz +chmod +x netbird-idp-migrate +``` + +Available architectures: `linux_amd64`, `linux_arm64`, `linux_arm`. + +**Option B — Build from source** (requires Go 1.25+ and a C compiler for CGO/SQLite): + +```bash +go build -o netbird-idp-migrate ./tools/idp-migrate/ +``` + +Copy the binary to the management server host if you built it elsewhere. + +--- + +## Step 2: Prepare Your Provider (if required) + + +**Auto-detected providers (Auth0, Azure AD, Keycloak, Okta, Authentik, PocketID, Google):** Skip this step — proceed to [Step 3](#step-3-stop-the-management-server). + + +### Zitadel + +Zitadel requires manual connector setup because the management server's service account credentials cannot be reused as OAuth client credentials for the Dex OIDC connector. + +1. Open the Zitadel console at `https:///ui/console`. +2. Go to **Projects** → select the NetBird project → **Applications**. +3. Click **New** and create an application: + - **Name:** `netbird-dex` + - **Type:** Web + - **Authentication Method:** Code +4. Set the redirect URI to `https:///oauth2/callback`. +5. Save and copy the **Client ID** and **Client Secret**. +6. Under **Token Settings**, enable both: + - User roles inside ID token + - User Info inside ID token +7. Create a `connector.json` file: + +```json +{ + "type": "zitadel", + "name": "zitadel", + "id": "zitadel", + "config": { + "issuer": "https://", + "clientID": "", + "clientSecret": "", + "redirectURI": "https:///oauth2/callback" + } +} +``` + +You will pass this file in Step 5 with the `--idp-seed-info` flag. + +See also: [Zitadel setup guide](/selfhosted/identity-providers/zitadel). + +### Custom / Unsupported Provider (JumpCloud, etc.) + +For providers without built-in detection, create a generic OIDC `connector.json`: + +```json +{ + "type": "oidc", + "name": "My Provider", + "id": "my-provider", + "config": { + "issuer": "https://idp.example.com", + "clientID": "my-client-id", + "clientSecret": "my-client-secret", + "redirectURI": "https:///oauth2/callback" + } +} +``` + +You will pass this file in Step 5 with the `--idp-seed-info` flag. + +--- + +## Step 3: Stop the Management Server + +**Systemd / bare-metal:** + +```bash +sudo systemctl stop netbird-management +``` + +**Docker Compose:** + +```bash +docker compose stop management +``` + +--- + +## Step 4: Back Up Your Data + +The tool creates `management.json.bak` automatically, but always make your own backups. + + +Do not skip this step. The migration modifies user IDs in the database. A manual backup is your only recovery path if something goes wrong. + + +**Systemd / bare-metal (SQLite):** + +```bash +cp /var/lib/netbird/store.db /var/lib/netbird/store.db.bak +cp /etc/netbird/management.json /etc/netbird/management.json.bak +``` + +**Docker Compose (SQLite in a named volume):** + +```bash +# Identify the volume name +VOLUME_NAME=$(docker volume ls --format '{{ .Name }}' | grep -i management) +echo "Volume: $VOLUME_NAME" + +# Get the host path +VOLUME_PATH=$(docker volume inspect "$VOLUME_NAME" --format '{{ .Mountpoint }}') +echo "Path: $VOLUME_PATH" + +# Verify store.db exists, then back up +sudo ls "$VOLUME_PATH/store.db" +sudo cp "$VOLUME_PATH/store.db" "$VOLUME_PATH/store.db.bak" +cp ~/netbird/management.json ~/netbird/management.json.bak +``` + +**PostgreSQL:** + +```bash +pg_dump -h -U -d -f netbird-backup.sql +cp /etc/netbird/management.json /etc/netbird/management.json.bak +``` + +--- + +## Step 5: Run the Migration + +### Dry run (always do this first) + +This previews what will happen without writing any changes. + +**Auto-detected providers:** + +```bash +./netbird-idp-migrate \ + --config /etc/netbird/management.json \ + --dry-run +``` + +**Zitadel / custom providers** (pass the `connector.json` from Step 2): + +```bash +./netbird-idp-migrate \ + --config /etc/netbird/management.json \ + --idp-seed-info "$(base64 < connector.json)" \ + --dry-run +``` + + +**Docker users:** If your database is in a volume that doesn't match the `Datadir` in `management.json`, add `--datadir`: + +```bash +./netbird-idp-migrate \ + --config ~/netbird/management.json \ + --datadir /var/lib/docker/volumes//_data \ + --dry-run +``` + + +You should see output like: + +``` +INFO resolved connector: type=oidc, id=auth0, name=auth0 +INFO found 12 total users: 12 pending migration, 0 already migrated +INFO [DRY RUN] would migrate user abc123 -> CgZhYmMxMjMSB3ppdGFkZWw (account: acct-1) +... +INFO [DRY RUN] migration summary: 12 users would be migrated, 0 already migrated +INFO derived domain for embedded IdP: mgmt.example.com +INFO [DRY RUN] new management.json would be: +{ ... } +``` + +Verify before proceeding: + +- Connector type and ID match your provider. +- User count matches what you expect. +- Generated config has the correct domain and endpoints. + +### Execute the migration + +Run the same command without `--dry-run`: + +```bash +# Auto-detected providers +./netbird-idp-migrate --config /etc/netbird/management.json + +# Zitadel / custom providers +./netbird-idp-migrate \ + --config /etc/netbird/management.json \ + --idp-seed-info "$(base64 < connector.json)" +``` + +The tool will show a summary and prompt for confirmation: + +``` +About to migrate 12 users. This cannot be easily undone. Continue? [y/N] +``` + +Type `y` and press Enter. + +### Review the new config + +Open `/etc/netbird/management.json` and verify: + +- `IdpManagerConfig` is **removed**. +- `EmbeddedIdP` is present with `"Enabled": true` and your connector in `StaticConnectors`. +- `HttpConfig.AuthIssuer` is `https:///oauth2`. +- `HttpConfig.AuthClientID` is `"netbird-dashboard"`. + +--- + +## Step 6: Post-Migration Configuration + +### Update your reverse proxy + +The embedded Dex IdP is served under `/oauth2/`. Your reverse proxy must route this path to the management server. + +**Caddy** — add to your `Caddyfile` inside the site block for your management domain: + +``` +reverse_proxy /oauth2/* management:80 +``` + +Place it alongside existing `/api/*` and `/management.ManagementService/*` routes, then reload: + +```bash +docker compose restart caddy +# or +sudo systemctl reload caddy +``` + +**Nginx:** + +```nginx +location /oauth2/ { + proxy_pass http://management:80; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} +``` + +Reload nginx after adding the route. + +**Traefik:** Add a route matching the `/oauth2/` path prefix, forwarding to the management service. + +**Verify the route works:** + +```bash +curl -s https:///oauth2/.well-known/openid-configuration | head -5 +``` + +Expected: a JSON response with `"issuer": "https:///oauth2"`. + +### Update dashboard environment + +If your dashboard uses a separate `dashboard.env` or environment variables, update the OAuth settings: + +```bash +# Before (external IdP) +AUTH_AUTHORITY=https://external-idp.example.com +AUTH_CLIENT_ID=old-client-id +AUTH_AUDIENCE=old-audience + +# After (embedded Dex) +AUTH_AUTHORITY=https:///oauth2 +AUTH_CLIENT_ID=netbird-dashboard +AUTH_AUDIENCE=netbird-dashboard +``` + +Restart the dashboard after updating. + +--- + +## Step 7: Start and Verify + +### Start the management server + +```bash +# Systemd +sudo systemctl start netbird-management + +# Docker Compose +docker compose up -d management +``` + +### Verify everything works + +1. **OIDC discovery:** Open `https:///oauth2/.well-known/openid-configuration` — it should return valid JSON. +2. **Dashboard login:** Log in to the dashboard — you should be redirected through your external IdP as before. +3. **Data integrity:** Check that peers are visible and policies are intact. + + +Use an incognito/private browser window or clear cookies for your first login. Stale tokens from the old IdP will fail validation. + + +--- + +## Command Reference + +``` +Usage: netbird-idp-migrate [flags] + +Flags: + --config string Path to management.json (required) + --datadir string Override data directory from config + --idp-seed-info string Base64-encoded connector JSON (overrides auto-detection) + --dry-run Preview changes without writing + --force Skip confirmation prompt + --skip-config Skip config generation (DB migration only) + --log-level string Log level: debug, info, warn, error (default "info") +``` + +--- + +## Advanced Scenarios + +### DB-only migration (manual config editing) + +Migrate user IDs in the database but skip config generation: + +```bash +./netbird-idp-migrate \ + --config /etc/netbird/management.json \ + --skip-config +``` + +### Non-interactive (CI / scripts) + +```bash +./netbird-idp-migrate \ + --config /etc/netbird/management.json \ + --force +``` + +--- + +## Troubleshooting + +### "store does not support migration operations" + +The store implementation is missing the required `ListUsers`/`UpdateUserID` methods. Upgrade to v0.66.4+ binaries. + +### "could not determine domain" + +The tool couldn't infer your management server's domain. Either set `HttpConfig.LetsEncryptDomain` in `management.json` before running, or use `--skip-config` and configure the embedded IdP section manually. + +### "could not open activity store" + +This is a **warning**, not an error. If `events.db` doesn't exist (e.g., fresh install), activity event migration is skipped. User ID migration in the main database still proceeds normally. + +### "no connector configuration found" + +No IdP configuration was detected. Provide it explicitly with `--idp-seed-info`, set the `IDP_SEED_INFO` env var, or ensure `IdpManagerConfig` is present in `management.json`. + +### "zitadel auto-detection is not supported" + +Zitadel's management config uses service account credentials that aren't valid OAuth client credentials. Follow the [Zitadel setup](#zitadel) in Step 2 to create a dedicated OAuth application. + +### "no client secret found" + +The Dex OIDC connector requires a confidential OAuth client with a client secret. If `IdpManagerConfig.ClientConfig.ClientSecret` is empty in your config, provide the connector credentials via `--idp-seed-info`. + +### "Errors.App.NotFound" from Zitadel after migration + +The dashboard is still redirecting to Zitadel's `/oauth/v2/` endpoint instead of the management server's `/oauth2` endpoint. Set `AUTH_AUTHORITY=https:///oauth2` in your dashboard environment — see [Update dashboard environment](#update-dashboard-environment). + +### OIDC discovery returns 404 + +The `/oauth2/` path is not being routed to the management server. Add a reverse proxy route — see [Update your reverse proxy](#update-your-reverse-proxy). + +### "jumpcloud does not have a supported Dex connector type" + +JumpCloud has no native Dex connector. Configure a generic OIDC connector manually with `--idp-seed-info` — see [Custom / Unsupported Provider](#custom--unsupported-provider-jumpcloud-etc) in Step 2. + +### "failed to create embedded IDP service: cannot disable local authentication..." + +The embedded IdP didn't support `StaticConnectors` in this config version. Upgrade to v0.66.4+ which includes this fix. + +### Partial failure / re-running + +The migration is **idempotent**. Already-migrated users are detected and skipped. If the tool fails partway through, fix the underlying issue and re-run — it picks up where it left off. + +--- + +## Rolling Back + +If something goes wrong after migration: + +1. **Stop** the management server. +2. **Restore the database:** + - SQLite (bare-metal): `cp /var/lib/netbird/store.db.bak /var/lib/netbird/store.db` + - SQLite (Docker volume): `sudo cp $VOLUME_PATH/store.db.bak $VOLUME_PATH/store.db` + - PostgreSQL: restore from your `pg_dump` backup +3. **Restore the config:** `cp /etc/netbird/management.json.bak /etc/netbird/management.json` +4. **Revert** any reverse proxy or dashboard env changes. +5. **Start** the management server.