This is the minimum security review Carrier expects before calling the current surface release-ready. It documents what the runtime does today, what is tested, and where users still need discipline.
Primary trust boundaries:
- inbound HTTP requests
- JWT bearer tokens
- generated SQL hitting Postgres
- pooled Postgres connections carrying per-request policy context
- idempotent write routes
- outbound HTTP client calls
Primary assets:
- model data stored in Postgres
- Carrier system tables (
carrier_idempotency_keys,carrier_jobs,carrier_events,carrier_event_deliveries,carrier_audit_log,carrier_schedule_runs) - authenticated user context (
CurrentUser) - audit/event history
Carrier's JWT contract today:
- algorithm allowlist is pinned to
HS256by default, orRS256whenJWT_ALGORITHM=RS256 - issuer and audience are validated
- access tokens are distinguished from refresh tokens via
token_type - clock skew leeway is configurable through
JWT_CLOCK_SKEW_SECONDS- default:
30
- default:
- secret rotation is supported by:
JWT_SECRETfor new token issuanceJWT_SECRET_PREVIOUSas a comma-separated validation fallback list
- RS256 verification uses
JWT_JWKS_URLwhen set, otherwise Carrier derives the Keycloak-style JWKS URL from the issuer as{issuer}/protocol/openid-connect/certs - JWKS responses are cached for
JWT_JWKS_CACHE_TTL_SECONDS- default:
300
- default:
Security implications:
- unsigned or wrong-algorithm tokens are rejected because verification is locked to the configured algorithm
- rotated tokens remain valid during the overlap window as long as the old secret stays in
JWT_SECRET_PREVIOUS - removing an old secret immediately invalidates tokens signed with it
- built-in Carrier token issuance is HMAC-only; when using RS256, issue tokens through the external identity provider such as Keycloak
Carrier's expanded auth helpers inherit the same local-first posture as the starter JWT flow:
- password hashing uses Argon2
- built-in password hashing rejects weak passwords by default
- TOTP secrets are generated from secure random bytes and encoded for authenticator-app compatible otpauth URIs
- magic links are signed and verified as one-time opaque tokens
- OAuth authorization helpers issue PKCE
S256challenges and one-time callback state
Security implications:
- built-in replay protection for magic links and OAuth callback state is currently in-process
- multi-instance production deployments should replace or wrap that replay storage with shared durable state
- keep password, magic-link, OAuth client, and provider secrets in environment/config values, not source files
Carrier-generated CRUD queries and DB helper invocations use bound parameters. User input is passed as values, not interpolated into SQL text.
What is safe by construction:
- generated CRUD filters
- generated list/search pagination parameters
db.fn_*helper argument passing- typed external client query/body serialization
What still requires discipline:
sql.one_as,sql.list_as, and related raw SQL helpers take authored SQL text directly- do not concatenate user-controlled strings into raw SQL text
- keep raw SQL statements static and bind user data through helper arguments or generated CRUD
Carrier sets these Postgres session values before DB work:
carrier.current_rolescarrier.current_tenantcarrier.current_user
The runtime now has a test that exercises connection reuse and verifies that calling
apply_policy_context(..., None) clears prior request state. Public or unauthenticated DB paths
must still call the policy-context helper before query execution.
The runtime now serializes concurrent first-writers for the same key by:
- deriving a deterministic advisory-lock key from
(route_scope, idempotency_key) - taking a Postgres advisory lock before checking/storing the key
- releasing followers only after the winning request stores the response
Scope construction is caller-aware:
- public routes:
{METHOD} {PATH}:public - authenticated routes:
{METHOD} {PATH}:user:{id}:tenant:{tenant}:workspace:{workspace}
That keeps idempotency replays scoped to the caller context instead of leaking across tenants.
Carrier currently records durable audit rows for:
- generated CRUD
create - generated CRUD
update - generated CRUD
delete - generated CRUD
restore - explicit
audit.record(...)calls
Each generated audit row is linked into a SHA-256 hash chain with prev_hash and row_hash.
Operators can run carrier compliance verify-audit-chain to prove the hot audit table is intact or
identify the first detected break. For retention compaction, archive a contiguous verified prefix
with its final chain head before removing it from the hot table.
Operational note:
- policy-denied attempts should still be captured in structured request logs
- durable audit rows for every possible deny path are not yet automatic across all route shapes
- rotate JWT secrets with overlap instead of cutover-only swaps
- treat raw SQL helpers as privileged escape hatches
- keep
Idempotency-Keygeneration client-side and stable across retries - review generated migrations before production rollout
- ship structured logs and audit/event tables into your platform observability stack