From ee60d7b0bd2339f92f81ff39c7a110207eee9686 Mon Sep 17 00:00:00 2001 From: jnfrati Date: Tue, 17 Mar 2026 17:59:03 +0100 Subject: [PATCH 1/7] added migration guide from external to embedded idp --- src/components/NavigationDocs.jsx | 4 + .../migration/combined-container.mdx | 2 +- .../migration/external-to-embedded-idp.mdx | 482 ++++++++++++++++++ 3 files changed, 487 insertions(+), 1 deletion(-) create mode 100644 src/pages/selfhosted/migration/external-to-embedded-idp.mdx 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. From 4028aa962200e2fb15dd984613555c9fb8ec35d0 Mon Sep 17 00:00:00 2001 From: jnfrati Date: Thu, 19 Mar 2026 10:28:01 +0100 Subject: [PATCH 2/7] updated guide --- .../migration/external-to-embedded-idp.mdx | 91 ++++++++++--------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/src/pages/selfhosted/migration/external-to-embedded-idp.mdx b/src/pages/selfhosted/migration/external-to-embedded-idp.mdx index 1685411b..498078ea 100644 --- a/src/pages/selfhosted/migration/external-to-embedded-idp.mdx +++ b/src/pages/selfhosted/migration/external-to-embedded-idp.mdx @@ -1,10 +1,10 @@ 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.' +export const description = 'Migrate a self-hosted NetBird deployment from an external identity provider to the embedded Dex-based IdP introduced in v0.62.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. +This guide walks through migrating a self-hosted NetBird deployment from an external identity provider to the embedded Dex-based IdP introduced in v0.62.0. **Who is this guide for?** This migration guide is for users who: @@ -103,12 +103,12 @@ Zitadel requires manual connector setup because the management server's service - **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**. +4. Set the redirect URI to `https:///oauth2/callback` and create the application. +5. After creating, you'll be prompted with the **Client ID** and **Client Secret**, make sure you copy them and save them, we'll use them later. 6. Under **Token Settings**, enable both: - User roles inside ID token - User Info inside ID token -7. Create a `connector.json` file: +7. Create a `connector.json` file with the values from the step 5 above: ```json { @@ -116,10 +116,10 @@ Zitadel requires manual connector setup because the management server's service "name": "zitadel", "id": "zitadel", "config": { - "issuer": "https://", - "clientID": "", - "clientSecret": "", - "redirectURI": "https:///oauth2/callback" + "issuer": "https://", // same as you use to log in to zitadel dashboard + "clientID": "", // from step 5 + "clientSecret": "", // from step 5 + "redirectURI": "https:///oauth2/callback" // same as you use to log in to management dashboard } } ``` @@ -152,18 +152,14 @@ 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 ``` + +**Bare-metal:** Depending on your setup, you might need to use systemctl or some other method to stop the management server. + + --- ## Step 4: Back Up Your Data @@ -174,13 +170,6 @@ The tool creates `management.json.bak` automatically, but always make your own b 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 @@ -202,7 +191,16 @@ cp ~/netbird/management.json ~/netbird/management.json.bak ```bash pg_dump -h -U -d -f netbird-backup.sql +cp ~/netbird/management.json ~/netbird/management.json.bak +``` + +**Bare-metal (SQLite):** + +Baremetal installations will have a different path based on your setup, but the genreal idea is the same: + +```bash cp /etc/netbird/management.json /etc/netbird/management.json.bak +cp /var/lib/netbird/store.db /var/lib/netbird/store.db.bak ``` --- @@ -217,26 +215,32 @@ This previews what will happen without writing any changes. ```bash ./netbird-idp-migrate \ - --config /etc/netbird/management.json \ + --config ~/netbird/management.json \ + --datadir /var/lib/docker/volumes//_data \ --dry-run ``` **Zitadel / custom providers** (pass the `connector.json` from Step 2): +Make sure that the connector.json that you're passing here contains the same values as you used in Step 2 above. +The --idp-seed-info flag let's us configure the initial Static Connector that the management server will use to connect to the exteernal IdP. + ```bash ./netbird-idp-migrate \ - --config /etc/netbird/management.json \ + --config ~/netbird/management.json \ + --datadir /var/lib/docker/volumes//_data \ --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`: +The `--datadir` flag is needed when your database is in a Docker volume whose path doesn't match the `Datadir` in `management.json`. You can find the volume name with `docker volume ls --format '{{ .Name }}' | grep -i management` and the volume path with `docker volume inspect --format '{{ .Mountpoint }}'`. + +**Bare-metal / systemd users** can omit `--datadir` if the default data directory in `management.json` is correct: ```bash ./netbird-idp-migrate \ - --config ~/netbird/management.json \ - --datadir /var/lib/docker/volumes//_data \ + --config /etc/netbird/management.json \ --dry-run ``` @@ -266,11 +270,14 @@ Run the same command without `--dry-run`: ```bash # Auto-detected providers -./netbird-idp-migrate --config /etc/netbird/management.json +./netbird-idp-migrate \ + --config ~/netbird/management.json \ + --datadir /var/lib/docker/volumes//_data # Zitadel / custom providers ./netbird-idp-migrate \ - --config /etc/netbird/management.json \ + --config ~/netbird/management.json \ + --datadir /var/lib/docker/volumes//_data \ --idp-seed-info "$(base64 < connector.json)" ``` @@ -284,7 +291,7 @@ Type `y` and press Enter. ### Review the new config -Open `/etc/netbird/management.json` and verify: +Open your `management.json` (e.g., `~/netbird/management.json`) and verify: - `IdpManagerConfig` is **removed**. - `EmbeddedIdP` is present with `"Enabled": true` and your connector in `StaticConnectors`. @@ -353,7 +360,7 @@ AUTH_CLIENT_ID=netbird-dashboard AUTH_AUDIENCE=netbird-dashboard ``` -Restart the dashboard after updating. +If you're using docker, make sure to recreate the dashboard container to apply the new environment variables with `docker compose up -d dashboard` --- @@ -362,10 +369,6 @@ Restart the dashboard after updating. ### Start the management server ```bash -# Systemd -sudo systemctl start netbird-management - -# Docker Compose docker compose up -d management ``` @@ -406,7 +409,8 @@ Migrate user IDs in the database but skip config generation: ```bash ./netbird-idp-migrate \ - --config /etc/netbird/management.json \ + --config ~/netbird/management.json \ + --datadir /var/lib/docker/volumes//_data \ --skip-config ``` @@ -414,7 +418,8 @@ Migrate user IDs in the database but skip config generation: ```bash ./netbird-idp-migrate \ - --config /etc/netbird/management.json \ + --config ~/netbird/management.json \ + --datadir /var/lib/docker/volumes//_data \ --force ``` @@ -472,11 +477,11 @@ The migration is **idempotent**. Already-migrated users are detected and skipped If something goes wrong after migration: -1. **Stop** the management server. +1. **Stop** the management server: `docker compose stop management` 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` + - SQLite (bare-metal): `cp /var/lib/netbird/store.db.bak /var/lib/netbird/store.db` - PostgreSQL: restore from your `pg_dump` backup -3. **Restore the config:** `cp /etc/netbird/management.json.bak /etc/netbird/management.json` +3. **Restore the config:** `cp ~/netbird/management.json.bak ~/netbird/management.json` 4. **Revert** any reverse proxy or dashboard env changes. -5. **Start** the management server. +5. **Start** the management server: `docker compose up -d management` From c0893bba461e21975f5e701932c243e5bf9210f1 Mon Sep 17 00:00:00 2001 From: jnfrati Date: Thu, 19 Mar 2026 10:50:11 +0100 Subject: [PATCH 3/7] clarify issuer domain used --- src/pages/selfhosted/migration/external-to-embedded-idp.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/selfhosted/migration/external-to-embedded-idp.mdx b/src/pages/selfhosted/migration/external-to-embedded-idp.mdx index 498078ea..aa87342b 100644 --- a/src/pages/selfhosted/migration/external-to-embedded-idp.mdx +++ b/src/pages/selfhosted/migration/external-to-embedded-idp.mdx @@ -116,10 +116,10 @@ Zitadel requires manual connector setup because the management server's service "name": "zitadel", "id": "zitadel", "config": { - "issuer": "https://", // same as you use to log in to zitadel dashboard + "issuer": "https://", // Root domain of your zitadel instance, e.g. https://zitadel.example.com "clientID": "", // from step 5 "clientSecret": "", // from step 5 - "redirectURI": "https:///oauth2/callback" // same as you use to log in to management dashboard + "redirectURI": "https:///oauth2/callback" // same as you use to log in to management dashboard (e.g. https://management.example.com/oauth2/callback) } } ``` From 4394a666989209f4624308b846cb02b122ec86c8 Mon Sep 17 00:00:00 2001 From: jnfrati Date: Thu, 19 Mar 2026 11:19:07 +0100 Subject: [PATCH 4/7] improved docs structure and added clarifying notes --- .../migration/external-to-embedded-idp.mdx | 85 ++++++++++++------- 1 file changed, 52 insertions(+), 33 deletions(-) diff --git a/src/pages/selfhosted/migration/external-to-embedded-idp.mdx b/src/pages/selfhosted/migration/external-to-embedded-idp.mdx index aa87342b..02ed2e99 100644 --- a/src/pages/selfhosted/migration/external-to-embedded-idp.mdx +++ b/src/pages/selfhosted/migration/external-to-embedded-idp.mdx @@ -50,20 +50,34 @@ After migration, existing users keep logging in through the same external provid | Authentik | ✅ | OIDC | No | | PocketID | ✅ | OIDC | No | | Google | ✅ | Google | No | -| Zitadel | ❌ | Zitadel | Yes — see [Step 2](#step-2-prepare-your-provider-if-required) | +| Zitadel | ❌ | Zitadel | Yes — see [Step 3](#step-3-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. +- **Auto-detected provider** → Skip Step 3 entirely. The tool reads your `management.json` and builds the connector automatically. +- **Zitadel** → You must complete Step 3 to create an OAuth app and supply connector credentials. +- **JumpCloud or other unsupported provider** → You must complete Step 3 to provide a custom OIDC connector. --- -## Step 1: Get the Migration Tool +## Step 1: Prepare your Management Server + +Make sure your management server is on the latest version, otherwise management will not be able to properly parse the new `management.json` file generated by this migration tool. + +```bash +docker compose pull +docker compose up -d management +``` + + +Before starting the migration, it's also a good idea to log out of the dashboard, as you might get a "stale" token from the old IdP which can cause 401 errors. + +--- + +## Step 2: Get the Migration Tool **Option A — Download a pre-built binary:** @@ -87,10 +101,10 @@ Copy the binary to the management server host if you built it elsewhere. --- -## Step 2: Prepare Your Provider (if required) +## Step 3: 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). +**Auto-detected providers (Auth0, Azure AD, Keycloak, Okta, Authentik, PocketID, Google):** Skip this step — proceed to [Step 4](#step-4-stop-the-management-server). ### Zitadel @@ -108,7 +122,14 @@ Zitadel requires manual connector setup because the management server's service 6. Under **Token Settings**, enable both: - User roles inside ID token - User Info inside ID token -7. Create a `connector.json` file with the values from the step 5 above: +7. Create a `connector.json` file with the values from step 5 above: + + +The JSON below is an example, you'll need to adjust it to your setup. +- "issuer": is the root domain of your zitadel instance, make sure you don't have any trailing slashes (e.g. https://zitadel.example.com) +- "clientID" and "clientSecret": are the values you copied from the application you created in step 4. +- "redirectURI": is the same as you use to log in to management dashboard (e.g. https://management.example.com/oauth2/callback) + ```json { @@ -116,15 +137,15 @@ Zitadel requires manual connector setup because the management server's service "name": "zitadel", "id": "zitadel", "config": { - "issuer": "https://", // Root domain of your zitadel instance, e.g. https://zitadel.example.com - "clientID": "", // from step 5 - "clientSecret": "", // from step 5 - "redirectURI": "https:///oauth2/callback" // same as you use to log in to management dashboard (e.g. https://management.example.com/oauth2/callback) + "issuer": "https://", + "clientID": "", + "clientSecret": "", + "redirectURI": "https:///oauth2/callback" } } ``` -You will pass this file in Step 5 with the `--idp-seed-info` flag. +You will pass this file in Step 6 with the `--idp-seed-info` flag. See also: [Zitadel setup guide](/selfhosted/identity-providers/zitadel). @@ -146,11 +167,11 @@ For providers without built-in detection, create a generic OIDC `connector.json` } ``` -You will pass this file in Step 5 with the `--idp-seed-info` flag. +You will pass this file in Step 6 with the `--idp-seed-info` flag. --- -## Step 3: Stop the Management Server +## Step 4: Stop the Management Server ```bash docker compose stop management @@ -162,7 +183,7 @@ docker compose stop management --- -## Step 4: Back Up Your Data +## Step 5: Back Up Your Data The tool creates `management.json.bak` automatically, but always make your own backups. @@ -196,7 +217,7 @@ cp ~/netbird/management.json ~/netbird/management.json.bak **Bare-metal (SQLite):** -Baremetal installations will have a different path based on your setup, but the genreal idea is the same: +Baremetal installations will have a different path based on your setup, but the general idea is the same: ```bash cp /etc/netbird/management.json /etc/netbird/management.json.bak @@ -205,7 +226,7 @@ cp /var/lib/netbird/store.db /var/lib/netbird/store.db.bak --- -## Step 5: Run the Migration +## Step 6: Run the Migration ### Dry run (always do this first) @@ -216,19 +237,19 @@ This previews what will happen without writing any changes. ```bash ./netbird-idp-migrate \ --config ~/netbird/management.json \ - --datadir /var/lib/docker/volumes//_data \ + --datadir /var/lib/docker/volumes/$VOLUME_NAME/_data \ --dry-run ``` -**Zitadel / custom providers** (pass the `connector.json` from Step 2): +**Zitadel / custom providers** (pass the `connector.json` from Step 3): -Make sure that the connector.json that you're passing here contains the same values as you used in Step 2 above. -The --idp-seed-info flag let's us configure the initial Static Connector that the management server will use to connect to the exteernal IdP. +Make sure that the connector.json that you're passing here contains the same values as you used in Step 3 above. +The --idp-seed-info flag lets us configure the initial Static Connector that the management server will use to connect to the external IdP. ```bash ./netbird-idp-migrate \ --config ~/netbird/management.json \ - --datadir /var/lib/docker/volumes//_data \ + --datadir /var/lib/docker/volumes/$VOLUME_NAME/_data \ --idp-seed-info "$(base64 < connector.json)" \ --dry-run ``` @@ -300,7 +321,7 @@ Open your `management.json` (e.g., `~/netbird/management.json`) and verify: --- -## Step 6: Post-Migration Configuration +## Step 7: Post-Migration Configuration ### Update your reverse proxy @@ -316,8 +337,6 @@ Place it alongside existing `/api/*` and `/management.ManagementService/*` route ```bash docker compose restart caddy -# or -sudo systemctl reload caddy ``` **Nginx:** @@ -350,21 +369,21 @@ If your dashboard uses a separate `dashboard.env` or environment variables, upda ```bash # Before (external IdP) -AUTH_AUTHORITY=https://external-idp.example.com -AUTH_CLIENT_ID=old-client-id AUTH_AUDIENCE=old-audience +AUTH_CLIENT_ID=old-client-id +AUTH_AUTHORITY=https://external-idp.example.com # After (embedded Dex) -AUTH_AUTHORITY=https:///oauth2 -AUTH_CLIENT_ID=netbird-dashboard AUTH_AUDIENCE=netbird-dashboard +AUTH_CLIENT_ID=netbird-dashboard +AUTH_AUTHORITY=https:///oauth2 ``` If you're using docker, make sure to recreate the dashboard container to apply the new environment variables with `docker compose up -d dashboard` --- -## Step 7: Start and Verify +## Step 8: Start and Verify ### Start the management server @@ -445,7 +464,7 @@ No IdP configuration was detected. Provide it explicitly with `--idp-seed-info`, ### "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. +Zitadel's management config uses service account credentials that aren't valid OAuth client credentials. Follow the [Zitadel setup](#zitadel) in Step 3 to create a dedicated OAuth application. ### "no client secret found" @@ -461,7 +480,7 @@ The `/oauth2/` path is not being routed to the management server. Add a reverse ### "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. +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 3. ### "failed to create embedded IDP service: cannot disable local authentication..." From 820b61a876c34f29d73960666e6ab237dbfb3b74 Mon Sep 17 00:00:00 2001 From: jnfrati Date: Mon, 30 Mar 2026 17:11:46 +0200 Subject: [PATCH 5/7] updated guide to match latest binary --- .../migration/external-to-embedded-idp.mdx | 291 ++++++------------ 1 file changed, 101 insertions(+), 190 deletions(-) diff --git a/src/pages/selfhosted/migration/external-to-embedded-idp.mdx b/src/pages/selfhosted/migration/external-to-embedded-idp.mdx index 02ed2e99..c2e4b27e 100644 --- a/src/pages/selfhosted/migration/external-to-embedded-idp.mdx +++ b/src/pages/selfhosted/migration/external-to-embedded-idp.mdx @@ -14,13 +14,13 @@ This guide walks through migrating a self-hosted NetBird deployment from an exte ## 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. +The migration tool does two things: 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. @@ -30,6 +30,10 @@ After migration, existing users keep logging in through the same external provid ## Before You Begin + +This guide assumes you're using Docker and everything is run in the same bash session, if you're using a different setup, adjust the commands accordingly. + + ### Prerequisites | Requirement | Details | @@ -39,28 +43,30 @@ After migration, existing users keep logging in through the same external provid | 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 3](#step-3-prepare-your-provider-if-required) | -| JumpCloud | ❌ | — | No Dex connector; manual OIDC setup required | +### Flags and Environment variables - -**Which path do I follow?** +| Flag | Environment variable | Description | +|------|----------------------|-------------| +| `--domain` | `NETBIRD_DOMAIN` | Domain for both dashboard and API | +| `--dashboard-domain` | `NETBIRD_DASHBOARD_URL` | Dashboard domain (will override --domain) | +| `--api-domain` | `NETBIRD_API_URL` | API domain (will override --domain) | +| `--config` | `NETBIRD_CONFIG_PATH` | Path to management.json (required) | +| `--datadir` | `NETBIRD_DATA_DIR` | Override data directory from config (store.db path will be derived from this) | +| `--idp-seed-info` | `NETBIRD_IDP_SEED_INFO` | Base64-encoded connector JSON | +| `--dry-run` | `NETBIRD_DRY_RUN` | Preview changes without writing | +| `--force` | `NETBIRD_FORCE` | Skip confirmation prompt | +| `--skip-config` | `NETBIRD_SKIP_CONFIG` | Skip config generation (DB migration only) | +| `--skip-populate-user-info` | `NETBIRD_SKIP_POPULATE_USER_INFO` | Skip populating user info (user id migration only) | +| `--log-level` | `NETBIRD_LOG_LEVEL` | Log level: debug, info, warn, error (default "info") | -- **Auto-detected provider** → Skip Step 3 entirely. The tool reads your `management.json` and builds the connector automatically. -- **Zitadel** → You must complete Step 3 to create an OAuth app and supply connector credentials. -- **JumpCloud or other unsupported provider** → You must complete Step 3 to provide a custom OIDC connector. - + +When to use --domain vs --dashboard-domain vs --api-domain: +- If you have a single domain for both dashboard and API, use --domain +- If you don't have a reverse proxy in front of dashboard and API, make sure you use the domain + port combination for each. + For example, `--dashboard-domain demo.netbird.io` and `--api-domain demo.netbird.io:33073` + --- ## Step 1: Prepare your Management Server @@ -101,58 +107,29 @@ Copy the binary to the management server host if you built it elsewhere. --- -## Step 3: Prepare Your Provider (if required) - - -**Auto-detected providers (Auth0, Azure AD, Keycloak, Okta, Authentik, PocketID, Google):** Skip this step — proceed to [Step 4](#step-4-stop-the-management-server). - +## Step 3: Prepare Your Provider -### Zitadel +The new embedded IdP made the generation of the OIDC connector config easier, that's why we recommend generating a new application for your provider. -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. +When following the guides to generate the application, make sure you store the client ID and client Secret somewhere safe. You'll need them later. -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` and create the application. -5. After creating, you'll be prompted with the **Client ID** and **Client Secret**, make sure you copy them and save them, we'll use them later. -6. Under **Token Settings**, enable both: - - User roles inside ID token - - User Info inside ID token -7. Create a `connector.json` file with the values from step 5 above: - - -The JSON below is an example, you'll need to adjust it to your setup. -- "issuer": is the root domain of your zitadel instance, make sure you don't have any trailing slashes (e.g. https://zitadel.example.com) -- "clientID" and "clientSecret": are the values you copied from the application you created in step 4. -- "redirectURI": is the same as you use to log in to management dashboard (e.g. https://management.example.com/oauth2/callback) - - -```json -{ - "type": "zitadel", - "name": "zitadel", - "id": "zitadel", - "config": { - "issuer": "https://", - "clientID": "", - "clientSecret": "", - "redirectURI": "https:///oauth2/callback" - } -} -``` +To spare details in this guide, you can use the following guides to create the OIDC connector configuration for your provider: +- [Auth0](/selfhosted/identity-providers/managed/auth0) +- [Azure AD](/selfhosted/identity-providers/managed/microsoft-entra-id) +- [Keycloak](/selfhosted/identity-providers/managed/keycloak) +- [Okta](/selfhosted/identity-providers/managed/okta) +- [Authentik](/selfhosted/identity-providers/managed/authentik) +- [PocketID](/selfhosted/identity-providers/managed/pocketid) +- [Google Workspace](/selfhosted/identity-providers/managed/google-workspace) +- [JumpCloud](/selfhosted/identity-providers/managed/jumpcloud) +- [Zitadel](/selfhosted/identity-providers/managed/zitadel) -You will pass this file in Step 6 with the `--idp-seed-info` flag. -See also: [Zitadel setup guide](/selfhosted/identity-providers/zitadel). +### Creating the IdP Seed info -### Custom / Unsupported Provider (JumpCloud, etc.) - -For providers without built-in detection, create a generic OIDC `connector.json`: +With the client id and client secret from the previous step, you can create the `idp-seed-info` for the tool, which will be used to generate the OIDC connector config. +1. We'll create a new file "connector.json" with the following contents, make sure you remember where you save it: ```json { "type": "oidc", @@ -161,13 +138,23 @@ For providers without built-in detection, create a generic OIDC `connector.json` "config": { "issuer": "https://idp.example.com", "clientID": "my-client-id", - "clientSecret": "my-client-secret", - "redirectURI": "https:///oauth2/callback" + "clientSecret": "my-client-secret" } } ``` -You will pass this file in Step 6 with the `--idp-seed-info` flag. + +Using Zitadel as an example, the JSON should have the following values: +- "issuer": is the root domain of your zitadel instance, make sure you don't have any trailing slashes (e.g. https://zitadel.example.com) +- "clientID" and "clientSecret": are the values you copy when creating the OAuth app + + + +2. Encode and store it in the `NETBIRD_IDP_SEED_INFO` environment variable: + +```bash +NETBIRD_IDP_SEED_INFO=$(base64 < connector.json) +``` --- @@ -177,10 +164,6 @@ You will pass this file in Step 6 with the `--idp-seed-info` flag. docker compose stop management ``` - -**Bare-metal:** Depending on your setup, you might need to use systemctl or some other method to stop the management server. - - --- ## Step 5: Back Up Your Data @@ -195,76 +178,61 @@ Do not skip this step. The migration modifies user IDs in the database. A manual ```bash # Identify the volume name -VOLUME_NAME=$(docker volume ls --format '{{ .Name }}' | grep -i management) +VOLUME_NAME=$(docker volume ls --format '{{ .Name }}' | grep -Ei 'management|mgmt') echo "Volume: $VOLUME_NAME" # Get the host path -VOLUME_PATH=$(docker volume inspect "$VOLUME_NAME" --format '{{ .Mountpoint }}') -echo "Path: $VOLUME_PATH" +NETBIRD_DATA_DIR=$(docker volume inspect "$VOLUME_NAME" --format '{{ .Mountpoint }}') +echo "Path: $NETBIRD_DATA_DIR" # 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 -``` +sudo ls "$NETBIRD_DATA_DIR/store.db" +sudo cp "$NETBIRD_DATA_DIR/store.db" "$NETBIRD_DATA_DIR/store.db.bak" -**PostgreSQL:** +# Verify management.json exists, the path will vary based on your setup, then back up +NETBIRD_CONFIG_PATH="/management.json" +ls "$NETBIRD_CONFIG_PATH" -```bash -pg_dump -h -U -d -f netbird-backup.sql -cp ~/netbird/management.json ~/netbird/management.json.bak +cp "$NETBIRD_CONFIG_PATH" "$NETBIRD_CONFIG_PATH.bak" ``` - -**Bare-metal (SQLite):** - -Baremetal installations will have a different path based on your setup, but the general idea is the same: - -```bash -cp /etc/netbird/management.json /etc/netbird/management.json.bak -cp /var/lib/netbird/store.db /var/lib/netbird/store.db.bak -``` - --- ## Step 6: Run the Migration -### Dry run (always do this first) - -This previews what will happen without writing any changes. - -**Auto-detected providers:** +### Validate required env vars / flags ```bash -./netbird-idp-migrate \ - --config ~/netbird/management.json \ - --datadir /var/lib/docker/volumes/$VOLUME_NAME/_data \ - --dry-run +echo $NETBIRD_CONFIG_PATH +echo $NETBIRD_DATA_DIR +echo $NETBIRD_IDP_SEED_INFO | base64 -d ``` -**Zitadel / custom providers** (pass the `connector.json` from Step 3): - -Make sure that the connector.json that you're passing here contains the same values as you used in Step 3 above. -The --idp-seed-info flag lets us configure the initial Static Connector that the management server will use to connect to the external IdP. - -```bash -./netbird-idp-migrate \ - --config ~/netbird/management.json \ - --datadir /var/lib/docker/volumes/$VOLUME_NAME/_data \ - --idp-seed-info "$(base64 < connector.json)" \ - --dry-run +You should expect to see an output similar to this: +``` +/etc/netbird/management.json +/var/lib/docker/volumes/management_data/_data +{ + "type": "oidc", + "name": "my-provider", + "id": "my-provider", + "config": { + "issuer": "https://idp.example.com", + "clientID": "my-client-id", + "clientSecret": "my-client-secret", + } +} ``` - -The `--datadir` flag is needed when your database is in a Docker volume whose path doesn't match the `Datadir` in `management.json`. You can find the volume name with `docker volume ls --format '{{ .Name }}' | grep -i management` and the volume path with `docker volume inspect --format '{{ .Mountpoint }}'`. +If you don't see the expected output, please make sure you followed the steps in this guide, or that you use +the correct flags while running the tool. -**Bare-metal / systemd users** can omit `--datadir` if the default data directory in `management.json` is correct: +### Dry run (always do this first) + +Assuming that you've followed the steps in this guide, you should be able to run the tool with the following command: ```bash -./netbird-idp-migrate \ - --config /etc/netbird/management.json \ - --dry-run +./netbird-idp-migrate --domain mgmt.example.com --dry-run ``` - You should see output like: @@ -290,16 +258,7 @@ Verify before proceeding: Run the same command without `--dry-run`: ```bash -# Auto-detected providers -./netbird-idp-migrate \ - --config ~/netbird/management.json \ - --datadir /var/lib/docker/volumes//_data - -# Zitadel / custom providers -./netbird-idp-migrate \ - --config ~/netbird/management.json \ - --datadir /var/lib/docker/volumes//_data \ - --idp-seed-info "$(base64 < connector.json)" +./netbird-idp-migrate --domain mgmt.example.com ``` The tool will show a summary and prompt for confirmation: @@ -312,12 +271,10 @@ Type `y` and press Enter. ### Review the new config -Open your `management.json` (e.g., `~/netbird/management.json`) and verify: - +Your `management.json` should be significantly smaller now, and the OIDC connector config should be present in `StaticConnectors`. +Make sure you verify the following: - `IdpManagerConfig` is **removed**. - `EmbeddedIdP` is present with `"Enabled": true` and your connector in `StaticConnectors`. -- `HttpConfig.AuthIssuer` is `https:///oauth2`. -- `HttpConfig.AuthClientID` is `"netbird-dashboard"`. --- @@ -365,18 +322,18 @@ 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_AUDIENCE=old-audience -AUTH_CLIENT_ID=old-client-id -AUTH_AUTHORITY=https://external-idp.example.com +Your dashboard should also be updated to use the embedded IdP configuration, for this, make sure you update the +`dashboard.env` or environment variables (check docker compose file for reference). -# After (embedded Dex) +Your env vars should have the following values: +``` AUTH_AUDIENCE=netbird-dashboard AUTH_CLIENT_ID=netbird-dashboard AUTH_AUTHORITY=https:///oauth2 +AUTH_SUPPORTED_SCOPES=openid profile email groups +AUTH_REDIRECT_URI=/nb-auth +AUTH_SILENT_REDIRECT_URI=/nb-silent-auth + ``` If you're using docker, make sure to recreate the dashboard container to apply the new environment variables with `docker compose up -d dashboard` @@ -403,47 +360,6 @@ Use an incognito/private browser window or clear cookies for your first login. S --- -## 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 ~/netbird/management.json \ - --datadir /var/lib/docker/volumes//_data \ - --skip-config -``` - -### Non-interactive (CI / scripts) - -```bash -./netbird-idp-migrate \ - --config ~/netbird/management.json \ - --datadir /var/lib/docker/volumes//_data \ - --force -``` - ---- - ## Troubleshooting ### "store does not support migration operations" @@ -462,10 +378,6 @@ This is a **warning**, not an error. If `events.db` doesn't exist (e.g., fresh i 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 3 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`. @@ -480,7 +392,7 @@ The `/oauth2/` path is not being routed to the management server. Add a reverse ### "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 3. +JumpCloud has no native Dex connector. Configure a generic OIDC connector manually with `--idp-seed-info` — see [Other Providers](#other-providers-auth0-azure-ad-keycloak-okta-authentik-pocketid-google-jumpcloud-etc) in Step 3. ### "failed to create embedded IDP service: cannot disable local authentication..." @@ -498,9 +410,8 @@ If something goes wrong after migration: 1. **Stop** the management server: `docker compose stop management` 2. **Restore the database:** - - SQLite (Docker volume): `sudo cp $VOLUME_PATH/store.db.bak $VOLUME_PATH/store.db` - - SQLite (bare-metal): `cp /var/lib/netbird/store.db.bak /var/lib/netbird/store.db` + - SQLite (Docker volume): `sudo cp $NETBIRD_DATA_DIR/store.db.bak $NETBIRD_DATA_DIR/store.db` - PostgreSQL: restore from your `pg_dump` backup -3. **Restore the config:** `cp ~/netbird/management.json.bak ~/netbird/management.json` +3. **Restore the config:** `cp $NETBIRD_CONFIG_PATH.bak $NETBIRD_CONFIG_PATH` 4. **Revert** any reverse proxy or dashboard env changes. 5. **Start** the management server: `docker compose up -d management` From 86730a3cd9ba12d8306a6a73a00039a05e5deb36 Mon Sep 17 00:00:00 2001 From: jnfrati Date: Tue, 31 Mar 2026 15:04:42 +0200 Subject: [PATCH 6/7] link fixes and added extra psql instructions --- .../migration/external-to-embedded-idp.mdx | 78 +++++++++++-------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/src/pages/selfhosted/migration/external-to-embedded-idp.mdx b/src/pages/selfhosted/migration/external-to-embedded-idp.mdx index c2e4b27e..080766c6 100644 --- a/src/pages/selfhosted/migration/external-to-embedded-idp.mdx +++ b/src/pages/selfhosted/migration/external-to-embedded-idp.mdx @@ -8,7 +8,7 @@ This guide walks through migrating a self-hosted NetBird deployment from an exte **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) +- Have an existing self-hosted deployment using an **external IdP** - Want to move to the **embedded Dex-based IdP** for a simpler, self-contained authentication setup @@ -46,19 +46,19 @@ This guide assumes you're using Docker and everything is run in the same bash se ### Flags and Environment variables -| Flag | Environment variable | Description | -|------|----------------------|-------------| -| `--domain` | `NETBIRD_DOMAIN` | Domain for both dashboard and API | -| `--dashboard-domain` | `NETBIRD_DASHBOARD_URL` | Dashboard domain (will override --domain) | -| `--api-domain` | `NETBIRD_API_URL` | API domain (will override --domain) | -| `--config` | `NETBIRD_CONFIG_PATH` | Path to management.json (required) | -| `--datadir` | `NETBIRD_DATA_DIR` | Override data directory from config (store.db path will be derived from this) | -| `--idp-seed-info` | `NETBIRD_IDP_SEED_INFO` | Base64-encoded connector JSON | -| `--dry-run` | `NETBIRD_DRY_RUN` | Preview changes without writing | -| `--force` | `NETBIRD_FORCE` | Skip confirmation prompt | -| `--skip-config` | `NETBIRD_SKIP_CONFIG` | Skip config generation (DB migration only) | -| `--skip-populate-user-info` | `NETBIRD_SKIP_POPULATE_USER_INFO` | Skip populating user info (user id migration only) | -| `--log-level` | `NETBIRD_LOG_LEVEL` | Log level: debug, info, warn, error (default "info") | +| Flag | Environment variable | Description | Expected format | +|------|----------------------|-------------| --------------- | +| `--domain` | `NETBIRD_DOMAIN` | Domain for both dashboard and API | example.com | +| `--dashboard-url` | `NETBIRD_DASHBOARD_URL` | Dashboard domain (will override --domain) | example.com or example.com:33073 or https://example.com | +| `--api-url` | `NETBIRD_API_URL` | API domain (will override --domain) | example.com or example.com:33073 or https://example.com | +| `--config` | `NETBIRD_CONFIG_PATH` | Path to management.json (required) | /path/to/management.json | +| `--datadir` | `NETBIRD_DATA_DIR` | Override data directory from config (store.db path will be derived from this) | /path/to/datadir | +| `--idp-seed-info` | `NETBIRD_IDP_SEED_INFO` | Base64-encoded connector JSON | base64-encoded JSON string | +| `--dry-run` | `NETBIRD_DRY_RUN` | Preview changes without writing | true or false | +| `--force` | `NETBIRD_FORCE` | Skip confirmation prompt | true or false | +| `--skip-config` | `NETBIRD_SKIP_CONFIG` | Skip config generation (DB migration only) | true or false | +| `--skip-populate-user-info` | `NETBIRD_SKIP_POPULATE_USER_INFO` | Skip populating user info (user id migration only) | true or false | +| `--log-level` | `NETBIRD_LOG_LEVEL` | Log level: debug, info, warn, error (default "info") | debug, info, warn, error | @@ -114,15 +114,15 @@ The new embedded IdP made the generation of the OIDC connector config easier, th When following the guides to generate the application, make sure you store the client ID and client Secret somewhere safe. You'll need them later. To spare details in this guide, you can use the following guides to create the OIDC connector configuration for your provider: -- [Auth0](/selfhosted/identity-providers/managed/auth0) -- [Azure AD](/selfhosted/identity-providers/managed/microsoft-entra-id) -- [Keycloak](/selfhosted/identity-providers/managed/keycloak) -- [Okta](/selfhosted/identity-providers/managed/okta) -- [Authentik](/selfhosted/identity-providers/managed/authentik) -- [PocketID](/selfhosted/identity-providers/managed/pocketid) -- [Google Workspace](/selfhosted/identity-providers/managed/google-workspace) -- [JumpCloud](/selfhosted/identity-providers/managed/jumpcloud) -- [Zitadel](/selfhosted/identity-providers/managed/zitadel) +- Auth0 +- Azure AD +- Keycloak +- Okta +- Authentik +- PocketID +- Google Workspace +- JumpCloud +- Zitadel ### Creating the IdP Seed info @@ -153,7 +153,7 @@ Using Zitadel as an example, the JSON should have the following values: 2. Encode and store it in the `NETBIRD_IDP_SEED_INFO` environment variable: ```bash -NETBIRD_IDP_SEED_INFO=$(base64 < connector.json) +export NETBIRD_IDP_SEED_INFO=$(base64 < connector.json | tr -d '\n') ``` --- @@ -182,16 +182,16 @@ VOLUME_NAME=$(docker volume ls --format '{{ .Name }}' | grep -Ei 'management|mgm echo "Volume: $VOLUME_NAME" # Get the host path -NETBIRD_DATA_DIR=$(docker volume inspect "$VOLUME_NAME" --format '{{ .Mountpoint }}') +export NETBIRD_DATA_DIR=$(docker volume inspect "$VOLUME_NAME" --format '{{ .Mountpoint }}') echo "Path: $NETBIRD_DATA_DIR" -# Verify store.db exists, then back up +# (SQLite only) Verify store.db exists, then back up sudo ls "$NETBIRD_DATA_DIR/store.db" sudo cp "$NETBIRD_DATA_DIR/store.db" "$NETBIRD_DATA_DIR/store.db.bak" # Verify management.json exists, the path will vary based on your setup, then back up -NETBIRD_CONFIG_PATH="/management.json" -ls "$NETBIRD_CONFIG_PATH" +export NETBIRD_CONFIG_PATH="/management.json" +cat "$NETBIRD_CONFIG_PATH" cp "$NETBIRD_CONFIG_PATH" "$NETBIRD_CONFIG_PATH.bak" ``` @@ -223,6 +223,18 @@ You should expect to see an output similar to this: } ``` +### (PostgreSQL only) Verify that the database env var is set + +The postgres store engine requires the postgres container to expose the port over the host +so that the migration tool can connect to it. You can set the env var in your shell: + +```bash +# This should match the same env var content that is passed to the management server +export NB_STORE_ENGINE_POSTGRES_DSN="host=localhost port=5432 user=postgres password=postgres dbname=netbird sslmode=disable" +``` + + + If you don't see the expected output, please make sure you followed the steps in this guide, or that you use the correct flags while running the tool. @@ -234,7 +246,12 @@ Assuming that you've followed the steps in this guide, you should be able to run ./netbird-idp-migrate --domain mgmt.example.com --dry-run ``` -You should see output like: + +If the env vars are not set, you can use the flags listed at the [Flags and Environment variables](#flags-and-environment-variables) section. + + + +After running the dry run command you should see output like: ``` INFO resolved connector: type=oidc, id=auth0, name=auth0 @@ -326,14 +343,13 @@ Your dashboard should also be updated to use the embedded IdP configuration, for `dashboard.env` or environment variables (check docker compose file for reference). Your env vars should have the following values: -``` +```bash AUTH_AUDIENCE=netbird-dashboard AUTH_CLIENT_ID=netbird-dashboard AUTH_AUTHORITY=https:///oauth2 AUTH_SUPPORTED_SCOPES=openid profile email groups AUTH_REDIRECT_URI=/nb-auth AUTH_SILENT_REDIRECT_URI=/nb-silent-auth - ``` If you're using docker, make sure to recreate the dashboard container to apply the new environment variables with `docker compose up -d dashboard` From f3c3e4e9b253373dbadfbd57b47ea1aa0256becf Mon Sep 17 00:00:00 2001 From: jnfrati Date: Thu, 2 Apr 2026 11:02:57 +0200 Subject: [PATCH 7/7] update versions properly --- .../migration/external-to-embedded-idp.mdx | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/pages/selfhosted/migration/external-to-embedded-idp.mdx b/src/pages/selfhosted/migration/external-to-embedded-idp.mdx index 080766c6..f8e223c3 100644 --- a/src/pages/selfhosted/migration/external-to-embedded-idp.mdx +++ b/src/pages/selfhosted/migration/external-to-embedded-idp.mdx @@ -1,10 +1,10 @@ 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.62.0.' +export const description = 'Migrate a self-hosted NetBird deployment from an external identity provider to the embedded IdP introduced in v0.62.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.62.0. +This guide walks through migrating a self-hosted NetBird deployment from an external identity provider to the embedded IdP introduced in v0.62.0. **Who is this guide for?** This migration guide is for users who: @@ -38,7 +38,7 @@ This guide assumes you're using Docker and everything is run in the same bash se | Requirement | Details | |-------------|---------| -| NetBird version | v0.66.4 or later | +| NetBird version | v0.67.2 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 | @@ -90,7 +90,7 @@ Before starting the migration, it's also a good idea to log out of the dashboard ```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 + https://github.com/netbirdio/netbird/releases/download/v0.67.2/netbird-idp-migrate_0.67.2_linux_amd64.tar.gz tar xzf netbird-idp-migrate.tar.gz chmod +x netbird-idp-migrate ``` @@ -380,11 +380,7 @@ Use an incognito/private browser window or clear cookies for your first login. S ### "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. +The store implementation is missing the required `ListUsers`/`UpdateUserID` methods. Upgrade to v0.67.2+ binaries. ### "could not open activity store" @@ -392,11 +388,7 @@ This is a **warning**, not an error. If `events.db` doesn't exist (e.g., fresh i ### "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`. - -### "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`. +No IdP configuration was detected. Provide it explicitly with `--idp-seed-info`, or set the `IDP_SEED_INFO` env var. ### "Errors.App.NotFound" from Zitadel after migration @@ -412,7 +404,7 @@ JumpCloud has no native Dex connector. Configure a generic OIDC connector manual ### "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. +The embedded IdP didn't support `StaticConnectors` in this config version. Upgrade to v0.67.2+ which includes this fix. ### Partial failure / re-running