Skip to main content

Security Architecture

Deployment posture​

Riviamigo is not approved for direct Internet exposure. Production Compose binds the application behind a host firewall and authenticated gateway, and every shared deployment must place an authenticated tunnel or identity-aware reverse proxy in front of it. The outer gateway owns public TLS, certificate renewal, identity enforcement, and Internet-facing rate limits; Riviamigo login remains required behind it.

See the secure deployment runbook for the required gateway contract and verification steps.

The current internal audit evidence and release requirements are tracked in docs/security-audit.md.

Sensitive vulnerability reports must use the repository's private GitHub Security Advisory flow. Do not publish credentials, exploit details, production data, live telemetry, or precise vehicle locations in public issues.

Authentication​

  • JWT (RS256) with 15-minute access tokens
  • 30-day HttpOnly refresh tokens, rotated on use
  • API keys are SHA256-hashed, read-only, and bound to exactly one vehicle; keys never authorize dashboard, account, administrative, or vehicle-setting writes
  • Argon2 password hashing
  • Vehicle membership roles are capability boundaries: viewer is telemetry and history read-only, manager may run operational changes such as schedules and backfills, and owner alone manages credentials and membership.
  • Protected-route bootstrap uses POST /v1/auth/bootstrap, which returns fresh tokens when a valid refresh cookie exists and 204 No Content when no resumable session exists, so first-load logged-out state does not depend on a visible refresh 401.
  • The web app attempts one refresh on protected 401s, then emits a single auth-expired flow: toast, session clear, redirect to /login, and resume to the original in-app route after successful sign-in.
  • Optional OIDC SSO is configured by a super-user under Settings > Authentication. It uses the provider issuer/subject identity rather than email alone; local password login remains enabled by default and can be restored with the documented environment break-glass override.
  • OIDC client secrets are write-only and encrypted when stored in the database. Authentication settings data is excluded from recovery packages; identity mappings remain, so operators must re-enter and test the provider after restore.
  • OIDC does not replace the authenticated HTTPS gateway, add provider-driven role mapping, or make a directly exposed origin safe.

Transport Security​

  • Production nginx is an HTTP origin on port 8080, loopback-bound by default, not a public TLS endpoint. Non-loopback binding requires the explicit ALLOW_PUBLIC_ORIGIN_BIND=true opt-in.
  • Public HTTPS and HSTS are enforced by the authenticated outer gateway
  • Secure cookie flag enforced; COOKIE_INSECURE is local-development-only. The narrow ALLOW_INSECURE_LAN_HTTP_AUTH=true production exception accepts only documented private literal-IP HTTP origins and emits a startup warning.

Session and enrollment boundaries​

New JWTs identify a refresh-token family. Rotation is serialized on that family; replay of a consumed token revokes its descendants and immediately invalidates the family's access tokens. Existing pre-upgrade JWTs without a family retain their original short lifetime. Missing or disabled users fail authorization. Administrative disablement also revokes all existing session families, so reenabling the account requires a fresh login. New session issuance locks the enabled account while creating the family to serialize it with disablement. Browser renewals share a cross-tab lock without storing access or refresh tokens.

Enrollment rechecks the selected vehicle against the signed-in Rivian account before writing local data. First enrollment creates the owner; a verified user joining an existing vehicle receives Viewer access. Existing roles, telemetry credentials and collectors remain in place. Owners explicitly promote members.

Live sockets are admitted under per-account, vehicle and process quotas. They close at JWT expiration and recheck user, family and membership authorization every 30 seconds. Revoked membership closes with 4403; session expiration or revocation uses 4401. Messages are limited to 4 KiB and outbound sends to five seconds. The browser clears removed-vehicle state and renews expired sessions.

Resource bounds​

Full-density metrics stream every retained source point through 10,000-row database cursor chunks within a repeatable-read snapshot. The JSON contract and compact-mode sample selection remain unchanged. Admission, deadlines, bounded output buffers and disconnect cancellation constrain resource use. Metric and Grafana reads have database statement deadlines. Disconnects and request timeouts cancel the owned backend query; admission remains held until its transaction has drained and rolled back, including cancellation cleanup. Interrupted JSON bodies are failures and cannot become successful query data. Grafana retains its existing point limit and accepts at most 32 targets.

Basemap data has a shared 128 MiB cache budget and seven-day TTL. A bounded maintenance scan invalidates legacy permanent entries. Redis uses 192 MiB noeviction beneath its 256 MiB container limit; only basemap entries enter the application eviction index. An unavailable/full cache produces a cache miss.

Outbound destinations​

Weather, geocoder, OIDC and S3 connections validate current resolved addresses, pin approved sockets, preserve TLS hostnames, and disable environment proxies and redirect following. Mixed public/private DNS results and forbidden ranges fail closed. OIDC and S3 private exceptions are operator-owned environment CIDRs; custom S3 HTTP additionally requires the explicit trusted-LAN exception. Actual response bytes are capped for weather, geocoder, Rivian and OIDC HTTP responses, including chunked bodies.

Trusted recovery packages​

Recovery packages are trusted operator input. PostgreSQL archives execute SQL; a candidate on the same server is not an SQL sandbox. The restore engine uses a temporary non-superuser login with no database/role creation, replication, bypass-RLS, server-file or program privileges. Bootstrap credentials perform only candidate creation, known extension setup, Timescale pre/post hooks, baseline comparison, swap and cleanup. Restored objects stay owned by the unprivileged role, which becomes NOLOGIN with no password after preparation. An expired restore deadline explicitly terminates the job role's sessions, disables its login/password, and removes its candidate and orphaned role. Archive functions and background-job definitions still require trust in the backup producer; arbitrary SQL risk is mitigated, not eliminated.

One compiled policy controls dump redaction, restore TOC filtering and candidate sanitation. Provider credentials, cryptographic keys, sessions, API keys, invitation proofs and operational target rows cannot be imported as active data. Authentication resets to OIDC off/password login on. Validated non-secret backup settings and inert history are restored separately; target history wins. Catalog paths never grant filesystem authority: only ordinary package files under the configured root are opened/deleted through directory handles. Remote materialization counts bytes, checks reserved free space, enforces a deadline and removes partial files. Metadata limits are independent of dump size. Once admitted, bounded upload/preflight/start work owns its lifetime through cleanup even if the HTTP client disconnects. Published preflight staging is removed automatically; only a durable restore handoff retains its package.

Rate Limiting​

  • Riviamigo applies class-specific auth, read, write, and heavy-read limits
  • The authenticated outer gateway must apply its own client-facing limits
  • The internal origin does not trust arbitrary forwarded client-IP headers

Headers​

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: no-referrer
  • The app shell uses a restrictive Content-Security-Policy; its connect-src permits same-origin services, WebSockets, and https://api.github.com for the optional browser-initiated stable release check. Its img-src permits blob: because protected vehicle artwork is fetched with the session token and rendered from in-memory object URLs; removing it silently replaces cached Rivian artwork with the packaged fallbacks.

Database​

  • PostgreSQL accessible only on internal Docker network (not exposed to host)
  • Parameterized queries via sqlx (compile-time checked)
  • Telemetry column names validated against allowlist before interpolation
  • Rivian vehicle credentials are encrypted with age before durable storage

Secret Storage​

  • Durable Rivian credential bundles are encrypted before storage in riviamigo.vehicle_credentials
  • Short-lived connect / OTP staging data should stay encrypted at rest in Redis and Redis should remain internal-only
  • Production may generate AGE_ENCRYPTION_KEY, JWT_SECRET, and JWT_PUBLIC_KEY on first start and persist them in PostgreSQL; externally managed overrides must supply all three together. The database-backed option is an explicitly accepted P2 shared-fate risk: compromise of the live database can expose locally generated keys. Recovery packages exclude these keys and credentials; restore generates fresh keys unless the complete external trio is supplied. A secret manager is the optional separate-custody recovery path.

Audit Logging​

  • Security events (first-owner claim, login success/failure, password changes, account-invitation operations, API-key create/revoke/rotate, and user administration) are recorded in riviamigo.security_events.
  • Each current event has an event type, actor when known, stable target, UUID request correlation when supplied, success/failure outcome, and redacted enum-like metadata. The service must not store credentials, tokens, email addresses, locations, or raw telemetry in this audit metadata.
  • The application retention worker purges live security events older than 365 days. Recovery packages may retain older events under the operator's separate backup-retention policy.
  • Owner-started ingestion captures are stored in riviamigo.vehicle_ingestion_capture_events. They hold sanitized ingestion facts, decoded values, and raw Parallax payload bytes for topics that cannot carry location or network identity. A key filter removes vehicle IDs, VINs, names, coordinates, and credential-like fields before storage and again on export. Each vehicle keeps one capture, which stops after one hour and is purged 24 hours after it stops.
  • Structured [riviamigo][LEVEL] key-value logs are written to stdout/stderr; Docker supplies the outer timestamp. The production wrapper normalizes Nginx error lines into the same shape.

Security regression controls​

  • pnpm security:routes verifies that every API route module is mounted through the intended protected router composition. The authentication module must have separate public, metadata, and protected mounts.
  • Restore capability tokens and agent keys are compared without early-exit string equality and reject oversized or malformed authentication headers.
  • Dynamic telemetry and metric selectors reject control characters and bounded oversized input before their allowlists are used to build SQL identifiers.

Dependencies​

  • Weekly automated dependency audits via Dependabot
  • cargo audit --deny warnings in CI
  • pnpm audit --prod --audit-level=high in CI
  • Semgrep SAST is blocking on trusted branches and same-repository pull requests; fork pull requests use a separate secret-free blocking scan.
  • Fixable critical and high Trivy findings are blocking after the unified production image builds; unfixed base-image findings remain visible for review and base-digest refreshes.
  • Workflow actions are pinned to reviewed commit SHAs.

Release Images​

  • Standard Compose pulls one public unified image from GitHub Container Registry; source builds use the explicit build overlay only.
  • Stable images use immutable Calendar Version tags and provenance attestations; latest is a moving convenience tag, not a reproducible deployment identifier.
  • Versioned container images are published only by intentional release workflows from validated main tags or the current dev pre-release candidate. The manually dispatched candidate workflow may also move the mutable :dev alias after it verifies the exact current upstream dev SHA; this alias is for development and test environments, not production release identity. Manual Candidate image dispatches may publish unversioned, commit-addressed build candidates; these are cache/release inputs, not releases. Stable and pre-release image tags and digests must be treated as release artifacts.
  • See the release images runbook for package visibility, tag protection, and recovery requirements.

Production Checklist​

  • COOKIE_INSECURE is NOT set (except local development)
  • ALLOW_INSECURE_LAN_HTTP_AUTH remains false, or the documented trusted-LAN exception and host firewall controls are in place
  • POSTGRES_PASSWORD changed from default
  • REDIS_PASSWORD is strong and Redis is not host-published
  • Recovery after generated-key replacement is tested, or all three explicit key overrides are stored safely
  • ALLOWED_ORIGINS set to exact frontend domain(s)
  • An authenticated tunnel or identity-aware reverse proxy terminates public HTTPS
  • Host firewall rules restrict direct access to port 8080
  • Redis is reachable only on a private/internal network
  • Firewall blocks API, PostgreSQL, Redis, and origin ports from external access
  • RIVIAMIGO_IMAGE uses the digest-qualified images.lock reference when exact repeatability matters, or IMAGE_TAG is pinned to a Calendar Version for version-level stability