From ee60d7b0bd2339f92f81ff39c7a110207eee9686 Mon Sep 17 00:00:00 2001 From: jnfrati Date: Tue, 17 Mar 2026 17:59:03 +0100 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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..."