# Admin token rotation `PYLON_ADMIN_TOKEN` authenticates every privileged route: - `/api/auth/session` in non-dev - `/api/auth/upgrade` in non-dev - `/api/admin/users/:id/export` (GDPR export) - `/api/admin/users/:id/purge` (GDPR delete) - `/api/sync/push` - Jobs / workflows / scheduler control planes - `/studio` in non-dev Treat it like an SSH key: minimum 32 bytes of randomness, never in git, rotate on any suspicion of compromise. ## Admin token vs. admin user `PYLON_ADMIN_TOKEN` is an **operator-role** secret — automation, cron, migrations, break-glass scripts. It's not a user identity; routes authenticated by the bearer don't have a `user_id`. For **designating a human as a platform admin**, use one of: 1. `auth: { user: { adminField: "isAdmin" } }` in the manifest + flip the field on the User row in Studio/SQL. The dispatcher reads it on every request. 2. `PYLON_ADMIN_EMAILS=eric@yapless.com,ops@acme.com` env var. Comma-separated allowlist of verified email addresses; matched users get `auth.is_admin` lifted on every successful auth resolution. Case-insensitive, requires `emailVerified=true`. When `adminField` is configured, the flag is persisted on first match so removing the email from the env doesn't demote — revoke explicitly via Studio/dashboard/SQL. Both paths apply on top of cookie/JWT/API-key auth, so admins sign in with their normal account and Studio respects the role. Don't use `PYLON_ADMIN_TOKEN` to "log in as admin" — see the "Don't use the admin token as a session token" guidance below. ## Without downtime (two-token rotation) The server only reads `PYLON_ADMIN_TOKEN` at startup. Rotation requires a restart. To do it without dropping traffic: 1. **Prepare**. Generate the new token: ```sh openssl rand -hex 32 > /etc/pylon/admin_token.new ``` 2. **Deploy side-by-side**. Start a new instance with the new token, let the load balancer health-check promote it, then drain + SIGTERM the old one. The 30-second in-flight window in `DEPLOY.md` covers admin calls that were mid-request. 3. **Update clients**. Any automation (CI, runbooks, cron, admin UIs) that hardcodes the old token must update. Grep for the old token prefix in Vault, 1Password, GitHub Actions secrets, Cloudflare environment, etc. before deleting the value. 4. **Verify + scrub**. Hit one admin endpoint with the new token; if it works, delete the old one from your secret store. ## Emergency (suspected compromise) 1. Generate a new token — skip no-downtime, it's not worth the risk: ```sh openssl rand -hex 32 > /etc/pylon/admin_token.new ``` 2. Revoke every active session and force re-login: ```sh curl -X POST -H "Authorization: Bearer $OLD_TOKEN" \ https://your-host/api/admin/sessions/purge ``` If you can't reach the admin API with the old token, stop the service and clear `PYLON_SESSION_DB`: ```sh systemctl stop pylon rm /var/lib/pylon/sessions.db* systemctl start pylon ``` 3. Rotate OAuth secrets too — same blast radius if the admin account was used to configure them. 4. Audit `audit_log` for the period the old token was valid. The `audit_log` plugin records who did what and when. 5. File an incident report per `SECURITY.md`. ## What NOT to do - Don't use the admin token as a session token — admin is not "a user", it's a break-glass credential. - Don't commit the token to git, even in a test fixture. The pre-commit hook rejects 32+ hex strings in tracked files. - Don't pass it as a URL query parameter. `Authorization: Bearer` only. URL params leak into proxy logs and browser history. - Don't reuse the token across environments. Staging ≠ prod.