mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-19 08:46:38 +00:00
152 lines
5.0 KiB
Go
152 lines
5.0 KiB
Go
// Command migrate-idp is a standalone CLI tool that migrates self-hosted NetBird
|
|
// deployments from an external IdP (Zitadel, Keycloak, Okta, etc.) to NetBird's
|
|
// embedded DEX-based IdP. It re-keys all user IDs in the database to match DEX's
|
|
// encoded format.
|
|
//
|
|
// Usage:
|
|
//
|
|
// migrate-idp --config /etc/netbird/management.json --connector-id oidc [--dry-run]
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
|
activitystore "github.com/netbirdio/netbird/management/server/activity/store"
|
|
"github.com/netbirdio/netbird/management/server/idp/migration"
|
|
"github.com/netbirdio/netbird/management/server/store"
|
|
"github.com/netbirdio/netbird/util"
|
|
"github.com/netbirdio/netbird/util/crypt"
|
|
)
|
|
|
|
func main() {
|
|
configPath := flag.String("config", "/etc/netbird/management.json", "path to management.json config file")
|
|
connectorID := flag.String("connector-id", "", "DEX connector ID to encode into user IDs (required)")
|
|
dryRun := flag.Bool("dry-run", false, "preview changes without writing to the database")
|
|
noBackup := flag.Bool("no-backup", false, "skip automatic database backup (SQLite only)")
|
|
logLevel := flag.String("log-level", "info", "log verbosity: debug, info, warn, error")
|
|
|
|
flag.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, `migrate-idp - Migrate NetBird user IDs from external IdP to embedded DEX
|
|
|
|
This tool re-keys all user IDs in the management database so they match DEX's
|
|
encoded format (base64-encoded protobuf with user ID + connector ID). Run this
|
|
with management stopped, then update management.json to enable EmbeddedIdP.
|
|
|
|
Service users (IsServiceUser=true) are re-keyed like all other users. All user
|
|
types will be looked up by DEX-encoded IDs after migration.
|
|
|
|
Usage:
|
|
migrate-idp --config /etc/netbird/management.json --connector-id oidc [flags]
|
|
|
|
Flags:
|
|
`)
|
|
flag.PrintDefaults()
|
|
|
|
fmt.Fprintf(os.Stderr, `
|
|
Migration procedure:
|
|
1. Stop management: systemctl stop netbird-management
|
|
2. Dry-run: migrate-idp --config <path> --connector-id <id> --dry-run
|
|
3. Run migration: migrate-idp --config <path> --connector-id <id>
|
|
4. Update management.json: Add EmbeddedIdP config with matching connector ID
|
|
5. Start management: systemctl start netbird-management
|
|
`)
|
|
}
|
|
|
|
flag.Parse()
|
|
|
|
level, err := log.ParseLevel(*logLevel)
|
|
if err != nil {
|
|
log.Fatalf("invalid log level %q: %v", *logLevel, err)
|
|
}
|
|
log.SetLevel(level)
|
|
|
|
if *connectorID == "" {
|
|
fmt.Fprintln(os.Stderr, "error: --connector-id is required")
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err := run(context.Background(), *configPath, *connectorID, *dryRun, *noBackup); err != nil {
|
|
log.Fatalf("migration failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func run(ctx context.Context, configPath, connectorID string, dryRun, noBackup bool) error {
|
|
// Load management config
|
|
config := &nbconfig.Config{}
|
|
if _, err := util.ReadJsonWithEnvSub(configPath, config); err != nil {
|
|
return fmt.Errorf("read config %s: %w", configPath, err)
|
|
}
|
|
|
|
if config.Datadir == "" {
|
|
return fmt.Errorf("config has empty Datadir")
|
|
}
|
|
|
|
log.Infof("loaded config from %s (datadir: %s, engine: %s)", configPath, config.Datadir, config.StoreConfig.Engine)
|
|
|
|
if dryRun {
|
|
log.Info("[DRY RUN] mode enabled — no changes will be written")
|
|
}
|
|
|
|
// Open main store
|
|
mainStore, err := store.NewStore(ctx, config.StoreConfig.Engine, config.Datadir, nil, false)
|
|
if err != nil {
|
|
return fmt.Errorf("open main store: %w", err)
|
|
}
|
|
defer mainStore.Close(ctx) //nolint:errcheck
|
|
|
|
// Set up field encryption for user data decryption
|
|
if config.DataStoreEncryptionKey != "" {
|
|
fieldEncrypt, err := crypt.NewFieldEncrypt(config.DataStoreEncryptionKey)
|
|
if err != nil {
|
|
return fmt.Errorf("create field encryptor: %w", err)
|
|
}
|
|
mainStore.SetFieldEncrypt(fieldEncrypt)
|
|
}
|
|
|
|
// Open activity store (optional — warn and continue if unavailable)
|
|
var actStore migration.ActivityStoreUpdater
|
|
activitySqlStore, err := activitystore.NewSqlStore(ctx, config.Datadir, config.DataStoreEncryptionKey)
|
|
if err != nil {
|
|
log.Warnf("could not open activity store, activity events will not be migrated: %v", err)
|
|
} else {
|
|
defer activitySqlStore.Close(ctx) //nolint:errcheck
|
|
actStore = activitySqlStore
|
|
}
|
|
|
|
// Backup databases before migration (unless --no-backup or --dry-run)
|
|
if !noBackup && !dryRun {
|
|
if err := backupDatabases(config.Datadir, config.StoreConfig.Engine); err != nil {
|
|
return fmt.Errorf("backup: %w", err)
|
|
}
|
|
}
|
|
|
|
// Run migration
|
|
result, err := migration.Migrate(ctx, &migration.Config{
|
|
ConnectorID: connectorID,
|
|
DryRun: dryRun,
|
|
MainStore: mainStore,
|
|
ActivityStore: actStore,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("\nMigration summary:\n")
|
|
fmt.Printf(" Migrated: %d users\n", result.Migrated)
|
|
fmt.Printf(" Skipped: %d users (already migrated)\n", result.Skipped)
|
|
if dryRun {
|
|
fmt.Printf("\n [DRY RUN] No changes were written. Remove --dry-run to apply.\n")
|
|
} else if result.Migrated > 0 {
|
|
fmt.Printf("\n Next step: update management.json to enable EmbeddedIdP with connector ID %q\n", connectorID)
|
|
}
|
|
|
|
return nil
|
|
}
|