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:
vieweris telemetry and history read-only,managermay run operational changes such as schedules and backfills, andowneralone manages credentials and membership. - Protected-route bootstrap uses
POST /v1/auth/bootstrap, which returns fresh tokens when a valid refresh cookie exists and204 No Contentwhen 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=trueopt-in. - Public HTTPS and HSTS are enforced by the authenticated outer gateway
Securecookie flag enforced;COOKIE_INSECUREis local-development-only. The narrowALLOW_INSECURE_LAN_HTTP_AUTH=trueproduction 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: nosniffX-Frame-Options: DENYReferrer-Policy: no-referrer- The app shell uses a restrictive
Content-Security-Policy; itsconnect-srcpermits same-origin services, WebSockets, andhttps://api.github.comfor the optional browser-initiated stable release check. Itsimg-srcpermitsblob: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, andJWT_PUBLIC_KEYon 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:routesverifies 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 warningsin CIpnpm audit --prod --audit-level=highin 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;
latestis a moving convenience tag, not a reproducible deployment identifier. - Versioned container images are published only by intentional release workflows from validated
maintags or the currentdevpre-release candidate. The manually dispatched candidate workflow may also move the mutable:devalias after it verifies the exact current upstreamdevSHA; 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_INSECUREis NOT set (except local development) -
ALLOW_INSECURE_LAN_HTTP_AUTHremainsfalse, or the documented trusted-LAN exception and host firewall controls are in place -
POSTGRES_PASSWORDchanged from default -
REDIS_PASSWORDis strong and Redis is not host-published - Recovery after generated-key replacement is tested, or all three explicit key overrides are stored safely
-
ALLOWED_ORIGINSset 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_IMAGEuses the digest-qualifiedimages.lockreference when exact repeatability matters, orIMAGE_TAGis pinned to a Calendar Version for version-level stability