Security & token storage
How the Pentify Terminal stores its bearer token, how the pairing handshake handles plaintext, how revocation works, and the threat model the design assumes.
Token model
The Terminal stores a Pentify-issued pt_live_* personal access token (PAT), nota Clerk session JWT. PATs are stable until revoked; Clerk JWTs need refresh and a browser cookie store, which a long-running terminal can’t sustain.
The PAT is bound to the user’s Clerk identity and a specific workspace. Switching workspaces requires a fresh pentify login. Every PAT lives in the pent_api_keys table as SHA-256(plaintext) only — the plaintext is never persisted server-side past the pairing TTL.
Storage hierarchy
The Terminal resolves the bearer token in this order on every command:
--api-token <pt_live_…>flag (single-command override).PENTIFY_API_TOKENenvironment variable (CI / headless / docker).- OS keychain — service
pentify-terminal, accountapi-token:- macOS: Keychain.
- Linux: Secret Service (libsecret). Falls back to
~/.config/pentify/credentialsmode0600if libsecret is unavailable, with a warning printed to stderr. - Windows: Credential Manager.
If none of the above resolves to a token, the Terminal prompts the user to run pentify login.
PENTIFY_API_TOKEN env var
Use this for CI, automation, and headless containers. The env var takes precedence over the keychain so a CI job overrides a developer’s local pairing on the same machine.
export PENTIFY_API_TOKEN=pt_live_…
pentify scans list # uses env, no login requiredIn CI, store the token in your runner’s secret store (GitHub Actions Secrets, GitLab Variables, etc.) — never commit it to source control. The token is a per-user PAT by default; for shared CI use a workspace key (mintable from Settings → API keys under “Workspace keys”).
Pairing plaintext lifecycle
The plaintext key crosses the wire exactly twice:
- Worker →
pent_terminal_pairings(server-side, after Clerk auth on the portal page). GET /auth/terminal/poll→ terminal → keychain (single-use, 5-minute TTL on the pairing row).
After step 2 the row is consumed (consumed_at set, plaintext deleted). Subsequent polls return 410 pairing_consumed. The plaintext is never logged in the Worker, never stored beyond those 5 minutes, and only ever moves end-to-end on TLS 1.3 with HSTS.
Revocation
- User-initiated: app.pentify.io/settings/api-keys → row → Revoke. Sets
revoked_at = now()server-side. - Effect on the Terminal: the next API call returns
401 invalid_api_key. The Terminal surfaces “Your Pentify token was revoked. Runpentify loginto re-pair.” - Re-pair on the same
device_id: server-side, the old PAT is auto-revoked when the new pairing row is written. So a user re-runningpentify logininvalidates the previous device’s token automatically. pentify logout: clears the LOCAL keychain only. Does not revoke server-side. Use the portal for proper revocation.
What never happens
- The Terminal never prints the token. Not in
--verboseoutput, not in error messages, not in stack traces. Logs that quote anAuthorizationheader MUST mask everything after thept_live_prefix. - The Terminal never persists the token in plaintext outside the OS keychain (or the documented
0600fallback file on bare Linux). - The Terminal never sends the token to anywhere other than
https://api.pentify.io/*. No telemetry, no error-reporting service, no third-party logger receives the bearer header.
Threat model notes
- Lost laptop: revoke at app.pentify.io/settings/api-keys. The token’s scopes give read-write on scans / targets / reports for that workspace; revocation cuts access immediately. Existing in-flight scans continue engine-side, but new commands fail.
- Compromised CI runner: revoke and rotate
PENTIFY_API_TOKENin the secret store. - Phishing the pairing flow: the pairing page lives on
app.pentify.io(Pentify-controlled domain) behind a Clerk session. An attacker can’t initiate pairing without a valid Pentify login. - Clerk session hijack:Clerk-side concern. Pentify’s blast radius is bounded by the user’s Pentify role and workspace.
Audit trail
Each PAT row in pent_api_keys records created_by_clerk_user_id, created_at, and last_used_at (updated on every successful auth). Workspace admins can review the table at Settings → API keys. Revoked keys keep last_used_at for forensic purposes.