Skip to main content

Security

This page covers the security practices and requirements for Proxima Console.

Secrets Management​

Environment Variables Only​

All secrets (database credentials, API keys, tokens) are stored in environment variables. Secrets must never appear in source code, configuration files, or commit history.

.env Files​

  • .env is listed in .gitignore and must never be committed.
  • .env.example is committed to the repository. It ships working local-dev defaults (database URL, ports, dev Vault token) rather than empty placeholders — treat every value in it as development-only and never as a template for production.
# .env.example
PROXIMA_DB_URL=
PROXIMA_NATS_URL=

API Authentication​

Multi-Mode Authentication​

The backend supports two authentication modes on all /api/v1/* routes:

  1. JWT Bearer tokens — User sessions with per-client RBAC permissions
  2. X-API-Key header — Service accounts with scoped permissions

See the Authentication page for full details.

JWT Security​

  • Access tokens: HS256 signed, 15-minute TTL
  • Refresh tokens: 32 cryptographically random bytes, SHA-256 hashed for storage, 7-day TTL
  • Token rotation on refresh: old refresh token invalidated, new one issued

Password Policy (NIST 800-63B)​

  • Minimum 12 characters, no complexity rules
  • 10,000 common password blocklist via go:embed
  • 5-password reuse prevention (bcrypt comparison against history table)

MFA (TOTP)​

  • TOTP secrets encrypted at rest with AES-256-GCM (32-byte key via PROXIMA_MFA_ENCRYPTION_KEY)
  • Recovery codes: 8 per user, bcrypt-hashed, single-use
  • MFA challenge tokens: single-use, 5-minute TTL, stored in Valkey

Progressive Lockout​

Failed login attempts trigger escalating lockout durations (1min at 5 failures, up to 1h at 20+). State stored in Valkey with in-memory fallback.

Rate Limiting​

API endpoints are rate-limited per IP address:

  • General API (/api/v1/*): 1000 requests/min (configurable via PROXIMA_API_RATE_LIMIT)
  • Public auth (login, refresh, forgot-password): 5 requests/min (configurable via PROXIMA_AUTH_RATE_LIMIT)
  • Protected auth routes (sessions, me, mfa) use the general API limit only

Email Enumeration Prevention​

  • Login returns the same 401 error for wrong email and wrong password
  • Forgot password always returns 200 regardless of email existence

Google OIDC SSO​

When configured (PROXIMA_GOOGLE_CLIENT_ID), users can authenticate with Google Workspace accounts:

  • State parameter: 32 random bytes, SHA-256 hashed, stored in Valkey with single-use GETDEL (5-minute TTL) to prevent CSRF
  • Domain restriction: hd claim enforced server-side in ID token validation (the URL hd hint alone is bypassable)
  • Email verification: only email_verified: true Google accounts are accepted
  • One-time auth code pattern: JWT tokens never appear in redirect URLs, preventing exposure in browser history and referrer headers

See the Authentication — Google OIDC SSO page for the full flow.

Service Account Security​

Service accounts use API keys for programmatic access:

  • Keys are generated as 32 cryptographically random bytes, base64url-encoded, prefixed with prxm_
  • Only the SHA-256 hash is stored in the database (and a 12-character prefix for identification)
  • The plaintext key is returned once at creation and cannot be retrieved again
  • Deactivated service accounts are filtered from API key lookups (is_active = TRUE in query)
  • Service accounts carry an optional expires_at. This is not enforced server-side — the column is nullable, the handler performs no validation, and the 1-year cap exists only in the create dialog. An account created through the API can have no expiry at all. Treat the cap as a UI convention, not an invariant.

Agent Authentication​

Each agent receives a unique NATS JWT with permissions scoped to its own subjects. Agents enroll over HTTPS using an install token (single-use by default, with a configurable max_uses for reusable fleet enrollment). The agent's private nkey never leaves the host. Credential renewal proves nkey possession over a single-use, server-issued challenge nonce (the challenge/nonce protocol applies to renewal, not the initial enroll). Compromised hosts can be individually revoked without affecting other agents. See NATS Security for the full architecture.

NATS Transport Security​

All NATS connections require mutual TLS 1.2+ with Vault-issued certificates:

  • Server certificates: Issued by Vault PKI (EC P-256) with 30-day max TTL, renewed via scripts/nats-cert-renew.sh
  • CA verification: Agents receive the CA certificate during enrollment and verify the NATS server's identity on every connection
  • Cipher suites: Restricted to ECDHE + AES-GCM (256 and 128-bit) — no CBC, no RSA key exchange
  • Client authentication: NATS verifies the agent's JWT signature against the account's signing key

Audit Logging​

All security-relevant actions are recorded in the audit_log table:

  • Authentication events (login success/failure, MFA verification, token refresh)
  • User lifecycle (creation, invitation, activation, password change, MFA enable/disable)
  • Admin operations (role changes, team membership, client assignments)
  • Session management (creation, revocation)

Each audit entry includes: actor ID, action, resource type/ID, details (JSON), IP address, and timestamp. Query via GET /api/v1/audit-log with audit:read and super-admin (a non-super-admin holding audit:read still gets 403; per-client visibility is GET /api/v1/admin/access-audit/by-client/{clientId}) permission.

Rules:

  • Never log secrets, tokens, or passwords in audit details
  • Always include the actor's IP address (from X-Forwarded-For or X-Real-IP for proxied requests)
  • Audit log entries are append-only (no updates or deletes)

SQL Injection Prevention​

All database queries use parameterized queries via sqlx. String concatenation is never used to build SQL queries.

// Correct -- parameterized query
err := db.GetContext(ctx, &host, "SELECT * FROM hosts WHERE id = $1", hostID)

// Wrong -- string concatenation (SQL injection risk)
query := fmt.Sprintf("SELECT * FROM hosts WHERE id = '%s'", hostID)

This rule applies to all query types: SELECT, INSERT, UPDATE, DELETE.

CORS​

Cross-Origin Resource Sharing is configured via the PROXIMA_CORS_ORIGINS environment variable. Only explicitly listed origins are allowed to make requests to the backend API.

# Allow specific origins
PROXIMA_CORS_ORIGINS=http://localhost:5173,https://console.proxima.example.com

File Permissions​

Agent Identity Files​

The agent creates identity files (agent.id, host_id) on the local filesystem. These files are created with 0600 permissions (owner read/write only) to prevent other users on the system from reading agent identifiers.

os.WriteFile(filepath, data, 0600)

Container Scanning​

Trivy in CI​

The CI pipeline runs Trivy to scan container images for known vulnerabilities. The scan is configured to block the pipeline on HIGH and CRITICAL severity findings.

# CI stage
container_scan:
script:
- trivy image --severity HIGH,CRITICAL --exit-code 1 $IMAGE

All container images must pass the Trivy scan before they can be deployed.

Systemd Security Hardening — not implemented​

The agent unit ships with no hardening directives

This section previously claimed the agent runs with NoNewPrivileges, ProtectSystem=strict, ProtectHome and PrivateTmp. It does not. Neither the shipped unit (infra/deploy/systemd/proxima-agent.service) nor the one the installer writes (scripts/install-agent.sh) sets any of them; PrivateTmp appears nowhere in the repo.

Adding them is worthwhile and tracked as a to-do. Until then, treat the agent as running with the privileges of its unit user and no systemd sandboxing, and size the blast radius accordingly.