Bootstrap the first admin
When you flip --auth-mode=enabled for the first time, you need an admin user to log in with. This page walks you through the POST /v1/admin/bootstrap flow.
The route is intentionally pre-auth — it has to be reachable when no users exist yet. It’s protected by:
- An
IsAdminBoundprobe — refuses if any admin role binding already exists at the default org scope. - The unique email constraint on
users.primary_email— a second call with the same email collides. - Operator network policy — you SHOULD restrict this route to a local Unix socket or loopback interface.
Prerequisites
-
--auth-mode=enabledset in config or on the command line.auth: mode: enabled master_key_file: /etc/nanosync/master.key -
Master key generated and readable by the server process.
openssl rand -hex 32 > /etc/nanosync/master.key chmod 0600 /etc/nanosync/master.key chown nanosync:nanosync /etc/nanosync/master.keyThe master key encrypts SSO client secrets and SAML private keys at rest via the
secrets_kvtable. Losing it means re-entering every secret. Back it up. -
Database reachable and migrations applied.
On first start, nanosync auto-runs
goose up. Confirm with:nanosync status
Two ways to bootstrap
The ns admin bootstrap command POSTs to the local server. Run it from the same host as the daemon:
ns admin bootstrap \
--email=admin@acme.com \
--password='S3cur3ly-Pick-A-Long-One' \
--display-name='Acme Admin'On success:
✓ admin created: user_id=00000000-0000-0000-0000-… email=admin@acme.comYou can omit --password to be prompted interactively (recommended — keeps the password out of shell history).
For ops automation or when you can’t run the CLI on the host:
curl -fsSL -X POST http://localhost:8080/v1/admin/bootstrap \
-H 'Content-Type: application/json' \
-d '{
"email": "admin@acme.com",
"password": "S3cur3ly-Pick-A-Long-One",
"display_name": "Acme Admin"
}'Successful response (201 Created):
{
"user_id": "00000000-0000-0000-0000-…",
"email": "admin@acme.com"
} Verify the bootstrap
Log in with the new account:
ns auth login --email=admin@acme.com
ns auth whoami
Expected output:
admin@acme.com
user_id: 00000000-0000-0000-0000-…
org_id: 00000000-0000-0000-0000-000000000001
system: false
Confirm the admin role binding exists:
ns role list --scope=org
You should see the admin built-in role bound to your user at the default org.
Common errors
| Status | Message | Cause | Fix |
|---|---|---|---|
| 503 | auth not configured | Server is running in --auth-mode=none | Restart with --auth-mode=enabled |
| 409 | bootstrap already completed | An admin user already exists | Use existing admin to manage further accounts |
| 400 | email and password required | Empty fields in request body | Provide both |
| 400 | invalid request body | Malformed JSON | Check Content-Type + JSON syntax |
| 500 | create user failed | DB-level error (often a stale connection) | Check server logs; verify DB connectivity |
Disaster recovery — admin credentials lost
If the only admin loses their password and you have direct DB access, you can recover by inserting a new password hash directly. Do this only when no other admin exists.
-- 1. Generate a new argon2id hash. Use any argon2 CLI / library:
-- argon2 'NewSecurePassword' -id -t 3 -m 16 -p 4 -e
--
-- 2. Update the existing admin user, or insert a new one + bind admin role.
-- Update existing:
UPDATE users
SET password_hash = '$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>',
password_algo = 'argon2id',
status = 'active'
WHERE primary_email = 'admin@acme.com';
-- Or insert a fresh admin (UUID generated by your client):
INSERT INTO users (id, kind, primary_email, display_name, status, password_hash, password_algo)
VALUES ('<new-uuid>', 'user', 'recovery@acme.com', 'Recovery Admin', 'active',
'$argon2id$v=19$m=…', 'argon2id');
INSERT INTO role_bindings (id, subject_type, subject_id, role_id, scope_type, scope_id)
VALUES ('<new-uuid>', 'user', '<new-uuid>',
'00000000-0000-0000-0000-0000000000a1', -- built-in admin role
'org',
'00000000-0000-0000-0000-000000000001'); -- default org
Then log in with the recovery account, audit the change, and revoke the old admin if compromised.
Every recovery action like this should be reflected in your incident log. The audit subsystem records the role binding insert if the daemon is up; if you did it via direct SQL while the daemon was down, write a note.
Security notes
- The bootstrap route is intentionally pre-auth. In production deployments, restrict it via reverse proxy or network policy so only operator-controlled networks can reach it. The simplest approach: expose the API on a private subnet and put
/v1/admin/bootstrapbehind your VPN. - After the first admin exists, the route still answers but always returns 409 — exploit value is zero.
- The password is never logged. Operator scripts that pass it on the command line are visible to other users on the box; prefer the interactive prompt.
What’s next
- RBAC roles & bindings — assign more granular roles to additional users.
- SSO setup — let users log in with Okta / Google / Azure AD instead of local passwords.
- Migration guide — promote an existing
--auth-mode=nonedeployment toenabledcleanly.