GitHub Dashboard →

Security

Authentication, secret management, network isolation, container hardening, and supply chain security.

Authentication and authorization

API keys

All SDK and server-to-server requests use scoped API keys prefixed vp_live_. Keys are stored as BLAKE3 hashes in the api_keys table — the plaintext is never persisted. A key can optionally be scoped to a single app, preventing cross-app access even within the same organisation.

OAuth 2.1 / OIDC 1.0

The admin panel authenticates via an embedded OAuth 2.1 server running inside the API process:

  • PKCE (S256 only) — prevents authorization code interception
  • Refresh token rotation — every refresh issues a new token pair; family tracking detects theft (entire family revoked on reuse)
  • Short-lived access tokens — 1-hour expiry
  • HS256 JWT signingJWT_SECRET, never stored in the DB

Password security

Passwords are hashed with Argon2id (memory-hard, GPU-resistant). No plaintext or reversible encoding is ever stored.

Multi-tenancy isolation

Every database row carries org_id. The auth middleware extracts it from the JWT or API key before every request, and every SQL query includes WHERE org_id = $1. Cross-tenant access is structurally impossible.

RBAC roles

RolePermissions
viewerRead campaigns and analytics
developer+ Manage apps, devices, API keys
admin+ Manage team members, webhooks
ownerFull access including billing and org deletion

Secret management

Local development

Secrets live in .env (gitignored). Never commit .env to git.

Kubernetes production

All secrets are stored in HashiCorp Vault at secret/visionpush/production and injected into pods via the External Secrets Operator. Sync runs every hour; secrets never touch the filesystem or git.

Vault keyRequired
DATABASE_URLYes
JWT_SECRETYes
CREDENTIAL_ENCRYPTION_KEYYes
AWS_ACCESS_KEY_IDYes
AWS_SECRET_ACCESS_KEYYes
S3_BUCKETYes
DB_PASSWORDYes
ALERTMANAGER_SMTP_PASSWORDIf alerting.enabled
ALERTMANAGER_SLACK_WEBHOOK_URLIf alerting.slack.enabled

Push credentials encryption

APNs p8 keys and FCM service account JSON are AES-256-GCM encrypted at rest in platform_configs using CREDENTIAL_ENCRYPTION_KEY. The key is never stored in the database — only in Vault / environment.

Network security

NetworkPolicies (zero-trust)

PodAllowed ingressAllowed egress
apiTraefik ingress controllerPostgres:5432, NATS:4222, S3/external:443
worker— (no inbound)Postgres:5432, NATS:4222, APNs/FCM/VAPID:443
adminTraefik ingress controller— (nginx, static files only)
postgresapi, worker, migrations
natsapi, worker

TLS everywhere

  • External traffic — TLS terminated at Traefik with Let's Encrypt (cert-manager)
  • APNs — TLS 1.2+ (HTTP/2 over TLS, required by Apple)
  • FCM — HTTPS to fcm.googleapis.com
  • Web Push — HTTPS to subscriber endpoints
  • Database — TLS enforced by CloudNativePG in-cluster
  • Vault — TLS for all ESO → Vault communication

Container security

All containers run with hardened security contexts:

Kubernetes securityContext
securityContext: runAsNonRoot: true allowPrivilegeEscalation: false readOnlyRootFilesystem: true # api, worker capabilities: drop: ["ALL"]
ℹ️
The admin nginx container drops all capabilities except NET_BIND_SERVICE and does not set readOnlyRootFilesystem (nginx writes to /var/cache/nginx). API and worker mount /tmp as emptyDir.

Supply chain security

Dependency scanning

  • Rustcargo audit in CI via rustsec/audit-check@v2, scanning the RustSec advisory database. A failed audit blocks the merge.
  • npm — Dependabot sends weekly PRs for outdated npm packages in admin/.
  • Cargo — Dependabot sends weekly PRs for Rust crates, grouped by minor-updates.
  • Docker — Dependabot monitors base image tags in infra/docker/.

Container image scanning

Trivy scans API and worker images in CI for OS and library CVEs. Results are uploaded to the GitHub Security tab as SARIF. Scan findings are visible in the Security dashboard without blocking the build.

Image provenance

All production images are tagged with both the git SHA (sha-XXXXXXX) and the semver tag (v1.2.3). ArgoCD Image Updater pins the deployed version — no latest tag in production.

Rate limiting

Per-API-key rate limiting uses NATS JetStream KV as a distributed counter, ensuring consistent limits across all API replicas.

  • Default: 1,000 requests/minute per API key
  • Rate-limited requests return HTTP 429 with a Retry-After header

Production hardening checklist

  • Rotate all secrets from generated defaults
  • Enable CORS_ORIGINS — do not leave as *
  • Enable serviceMonitor.enabled: true and configure alert routing
  • Enable networkPolicy.enabled: true (default in the chart)
  • Set postgres.instances: 2 for HA streaming replication
  • Configure WAL-G / CloudNativePG backup to an off-site S3 bucket
  • Enable alerting.enabled: true and configure email/Slack receivers
  • Review Trivy scan results in GitHub Security tab
  • Change the default admin password (admin@example.com / changeme)

Incident response

JWT or encryption key compromised

Shell
# 1. Generate new secrets NEW_JWT=$(openssl rand -base64 32) NEW_EK=$(openssl rand -base64 32) # 2. Update Vault vault kv put secret/visionpush/production \ JWT_SECRET="$NEW_JWT" \ CREDENTIAL_ENCRYPTION_KEY="$NEW_EK" # 3. Force ESO sync kubectl annotate externalsecret visionpush-secrets \ force-sync=$(date +%s) -n visionpush # 4. Restart pods (picks up new secret) kubectl rollout restart \ deployment/visionpush-api \ deployment/visionpush-worker \ -n visionpush
⚠️
All existing JWTs are immediately invalidated after restart. Admin panel users must log in again.

API key compromised

SQL
DELETE FROM api_keys WHERE id = 'compromised-key-id';

The key is rejected immediately on next use — no JWT, no cached state.