Migration guide — auth-mode=none → enabled

When you upgrade an existing single-tenant deployment to --auth-mode=enabled, three things have to be true before the flip:

  1. The database has the auth tables (migrations 008–014 applied).
  2. A master key is generated and the server can read it.
  3. An admin user exists and has been bound to the admin role.

The recommended sequence does all three first, then flips the flag — so you can roll back by un-flipping the flag without losing any data.


Compatibility matrix

Capability--auth-mode=none (today)--auth-mode=enabled (after migration)
Public APIyesno — 401 for anonymous
Login screen / user accountsn/arequired
RBAC enforcement on mutationsbypassedenforced
Project-scoped list filteringdisabledfiltered to active project
Audit actoralways "system"real user/token identity
Existing API tokens for CIn/acreated via ns token create, used as Bearer
SSOn/aOIDC + SAML available
Rollbackalways availablerestart with --auth-mode=none

Step-by-step

  1. Apply migrations 008–014.

    On the next start, nanosync auto-runs goose up. If you want to apply manually first to a maintenance window:

    ns admin migrate up
    # Or: goose -dir internal/db/migrations/postgres postgres "$DATABASE_URL" up

    Verify the seeded tenant:

    SELECT slug FROM orgs WHERE id = '00000000-0000-0000-0000-000000000001';
    -- ('nanosync')
    
    SELECT count(*) FROM projects WHERE id = '00000000-0000-0000-0000-000000000003';
    -- (1)

    Migration 013 backfilled every existing pipeline and connection with project_id = '00000000-0000-0000-0000-000000000003' (the default project). Confirm:

    SELECT count(*) FROM pipelines WHERE project_id IS NULL;
    -- (0)

    Migration 014 flips project_id to NOT NULL on pipelines, connections, and pipeline_runs on Postgres. SQLite enforces non-null at the application layer (it can’t ALTER COLUMN portably).

  2. Generate the master key.

    openssl rand -hex 32 > /etc/nanosync/master.key
    chmod 0600 /etc/nanosync/master.key
    chown nanosync:nanosync /etc/nanosync/master.key

    Back this up to your secrets vault. Losing it means losing every encrypted secret (SSO client secrets, SAML private keys). The key is NOT recoverable from the database — only from your backup.

  3. Update the config.

    auth:
      mode: enabled
      master_key_file: /etc/nanosync/master.key
      session_ttl: 12h

    Don’t restart yet. The next step needs the daemon running in its current mode.

  4. Bootstrap the first admin while still on --auth-mode=none.

    This is the trick that makes the migration zero-downtime: you bootstrap before you flip the flag, while the server still answers as SystemRoot. The POST /v1/admin/bootstrap route is always available regardless of mode, and it creates the admin user + binds the admin role.

    ns admin bootstrap \
      --email=admin@acme.com \
      --display-name='Acme Admin'
    # Prompts for password interactively

    Verify by logging in (still on --auth-mode=none, so this also succeeds):

    ns auth login --email=admin@acme.com
    ns auth whoami
  5. Restart with --auth-mode=enabled.

    systemctl restart nanosync

    Confirm anonymous requests are now refused:

    curl -sS -i http://localhost:8080/v1/pipelines
    # HTTP/1.1 401 Unauthorized
    # {"error":"unauthenticated"}

    And confirm the admin still works:

    ns auth login --email=admin@acme.com   # cookie now required by the server
    ns pipeline list
  6. Issue API tokens for any non-interactive callers.

    Existing CI scripts and automation hitting the API now need to send a Bearer token:

    ns token create --name=ci-deploys --ttl=8760h
    # Output:
    # ✓ token created — keep this secret, it will not be shown again
    #   token: ns_a1b2c3d4_VeryLongRandomSecretGoesHereYouOnlySeeItOnce…
    #   id:    01h…

    Update your CI to send Authorization: Bearer ns_… on every API call.

  7. Bind roles to additional users.

    Now that admin works, invite the rest of your team and bind them to roles:

    ns role bind editor --user=alice@acme.com \
      --scope=workspace --scope-id=<workspace-uuid>
    ns role bind operator --user=oncall@acme.com \
      --scope=workspace --scope-id=<workspace-uuid>

    See RBAC roles & bindings for the full role catalog and custom recipes.

  8. (Optional) Wire SSO.

    For larger teams, OIDC removes the password management burden. See SSO setup.


Rollback

If anything goes wrong, restart with --auth-mode=none:

systemctl stop nanosync
sed -i 's/^  mode: enabled/  mode: none/' /etc/nanosync/config.yaml
systemctl start nanosync

Data persists across the flag flip — users, role bindings, projects, audit events all stay. When you fix the issue and re-flip to enabled, everything is exactly as you left it.

The flag does NOT delete any data. There is no --reset-auth flag. Recovery is by restart, not by destructive operations.


What changes for existing automation

CallerBeforeAfter
ns CLI on the same hostunauthenticatedns auth login once + sessions auto-renew
ns CLI on a CI runnerunauthenticatedNS_TOKEN=ns_… env var (mint via ns token create)
curl https://nanosync/v1/...directcurl -H "Authorization: Bearer ns_…" https://…/v1/...
Web UIdirectlogin page, then cookie-bearing session
Terraform provider (community)directenv var NANOSYNC_TOKEN=ns_…
GitHub Actions / GitLab CIunauthenticatedsecret NS_TOKEN set in pipeline secrets, exported into env

Sessions are 12h by default (sliding) — interactive users rarely re-login. Tokens have configurable TTL via --ttl=... on ns token create.


Downtime expectations


Common gotchas

GotchaSymptomFix
Forgot to bootstrap before flipping the flagLocked out — 401 on every call, including from the CLIRestart with --auth-mode=none, run bootstrap, restart again with enabled
Reverse proxy stripping cookiesLogins “succeed” but next request is 401Allow nanosync_session cookie; verify SameSite=Lax works for your domain
Multi-instance setup with shared DBSessions issued by node A rejected by node BSessions are server-side — they’ll work across nodes, but verify the master key is identical on every node (encrypts session secrets)
Existing CI starts failing401 from every CI runMint tokens with ns token create, set env var in CI secrets
data_dir not writable by service userAudit sink fails to open the filechown nanosync:nanosync /var/lib/nanosync

What’s next