Pentify
Pentify Terminal

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:

  1. --api-token <pt_live_…> flag (single-command override).
  2. PENTIFY_API_TOKEN environment variable (CI / headless / docker).
  3. OS keychain — service pentify-terminal, account api-token:
    • macOS: Keychain.
    • Linux: Secret Service (libsecret). Falls back to ~/.config/pentify/credentials mode 0600 if 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 required

In 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:

  1. Worker → pent_terminal_pairings (server-side, after Clerk auth on the portal page).
  2. 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. Run pentify 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-running pentify login invalidates 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 --verbose output, not in error messages, not in stack traces. Logs that quote an Authorization header MUST mask everything after the pt_live_ prefix.
  • The Terminal never persists the token in plaintext outside the OS keychain (or the documented 0600 fallback 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_TOKEN in 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.

Related
Pairing UX walkthrough: Login & auth. Error code catalog: Troubleshooting. Backend key model: Authentication.