Security Model

The binary contains no secrets and grants no authority. Real protection comes from the API server. This page documents the boundary so you can reason about what the CLI does and doesn't defend against.

The boundary

Every request the CLI makes flows through three server-side gates:

  1. Token validation (Laravel Passport) — rejects any unknown / expired / revoked token.
  2. Tenant resolution (Stancl Tenancy) — ensures the request runs in the requesting tenant's database context.
  3. Permission check (Spatie Permissions) — enforces RBAC on every endpoint, same as the web app.

If any gate rejects, the request is over before any business logic runs. The CLI receives the rejection and translates to a typed exit code.

OAuth flow security properties

The default autocom auth login uses OAuth 2.0 authorization-code with PKCE (RFC 7636). Why each piece matters:

Property What it defends against
Public client (no secret) Public clients can't keep secrets — anything baked into the binary leaks. PKCE replaces client-secret as the proof-of-identity.
PKCE S256 A network attacker who intercepts the authorization code can't redeem it without the verifier (which only ever lives in the CLI process memory).
CSRF state token A malicious page that tricks the browser into hitting the callback can't poison the flow — the state is a fresh random per login.
Loopback redirect (127.0.0.1) Per RFC 8252 §7.3, loopback IP literals can't be DNS-spoofed. We use 127.0.0.1, never localhost.
Single-use callback The local listener accepts ONE request then shuts down. Stale browser tabs can't replay the code.
5-minute timeout Idle authorize sessions get cleaned up; the local port becomes available again.

What's stored where

Data Default storage What protects it
Access token OS keychain (autocom-cli service) Encrypted by OS — Keychain (macOS), Secret Service (Linux), Credential Manager (Windows)
Refresh token Same Same
Tokens (fallback) ~/.autocom/credentials (mode 0600) Filesystem permissions only — used only when no keychain reachable
User name + email Same as tokens Same
API URL + tenant ~/.autocom/config.toml (mode 0600) Filesystem permissions only
PKCE verifier Process memory only Never written to disk
OAuth client ID Built into binary at link time Public information — not a secret

Build-time hardening

Layer What we do
Symbol stripping -ldflags="-s -w" on every release — removes Go's debug metadata, cuts ~30% of binary size, makes string-grep reverse engineering harder
Reproducible paths -trimpath strips local filesystem paths from the binary so two builds of the same source produce identical bytes
Identifier obfuscation make build-garbled opt-in (uses garble) — mangles identifier names. Off by default because it breaks reflection and panics show garbled stacks
Distribution integrity SHA256 sums published with every release; signed Sigstore Cosign keyless attestation planned

What we don't do

Things people sometimes ask about that we deliberately don't ship:

  • "Encrypted binary at rest" — the decryption key has to ship with the binary. Anyone who can read the binary can extract the key. It's cosmetic. We skip the snake oil.
  • License keys baked into the binary — same problem, same answer.
  • Telemetry that includes content — the planned version-check ping is opt-out via no_telemetry = true and only sends (version, os, arch). Never any tenant data, command names, or arguments.
  • Local-only authority — the CLI never bypasses the server. There's no autocom orders force-cancel --offline or similar.

Threat model

What the CLI defends against, and what it explicitly doesn't:

Threat Defense
Network MitM intercepting tokens TLS to the API; OAuth code can't be redeemed without PKCE verifier
Stolen binary Binary is public — nothing to steal except your time reading the code
Stolen laptop with tokens in keychain Keychain is unlocked at user-login; if the laptop is unlocked, the attacker is you. Use FileVault / LUKS / BitLocker.
Compromised release artifact Verify SHA256SUMS against the GitLab Release entry before installing
Compromised npm/PyPI dependency (n/a here) We have zero JS/Python deps. Go modules are vendored into go.sum; Renovate would alert on changes
Phishing / fake authorize page The OAuth flow is bound to api_url you typed — if that URL is real, the page you see is real. Confirm the cert chain.
Logged shell history with PAT Use --token-stdin or auth login (OAuth) instead of --with-token <PAT>
ls /tmp/cli-state finds your token Don't put AUTOCOM_CREDENTIALS_FILE in a world-readable location. The default ~/.autocom/credentials is mode 0600

Auditability

Every mutation routed through the GraphQL gateway lands in module_bus_audit_log (see GraphQL Gateway → Mutation audit). The CLI doesn't add an extra log layer — what the server records is what happened. Operators investigating "what did the CLI do" pull from the same audit trail as web-app actions.

Reporting issues

If you find a security issue with the CLI, the API, or the OAuth flow, please email security@autocom.wexron.io rather than opening a public issue. We backport fixes to the previous minor release.