Operational practices
Day-to-day security at RateStack is a function of platform design, not heroics. The short version: persistent secrets are ciphertext, identity is opaque-session based, every state change is hash-chained, and every outbound integration is behind an SSRF-validated URL fetch.
Encryption
Persisted secrets are encrypted with AES-256-GCM via SecretEncryptor. Master keys are supplied via the PRICING_MASTER_KEY environment variable; we never persist the key itself. Online rotation requires both the old and new key to be present during the migration window — the encryptor reads with the previous key and writes with the current — and rekey endpoints re-encrypt rows in place. Once every row uses the new key, the previous key is dropped on the next service roll.
In transit, every external endpoint is TLS 1.2+. Internal service-to-service traffic optionally carries the X-Internal-Token header verified constant-time. Production environments require the token; development environments treat it as advisory.
Authentication & sessions
Email/password authentication uses bcrypt at cost 12 with a sliding reuse-window check that prevents recently-used passwords from being reused. SSO covers Google, Microsoft, and Apple — Apple's client_secret JWT is signed at runtime from an operator-supplied P-256 key (we never persist Apple's secret).
Session tokens are opaque random IDs (32 bytes, base64url-encoded). The browser receives them via an HttpOnly cookie; the server-side session store maps them to user identity, permissions, and the upstream auth token. We do not issue JWTs to browsers — the public-portal BFF pattern means client JavaScript never sees an access token.
Authorization (capability catalog)
Every authenticated call is checked against the capability catalog before it reaches business logic. The catalog ships 12 operational capabilities (loan.read, loan.write,loan.share, loan.price, ratesheet.activate,lock.commit, etc.) plus 7 provider capabilities for integration partners. The catalog is published at /v1/capabilities so partners can author scoped clients without reverse-engineering.
JWTs carry an org_roles claim listing the operator's roles and any active TTL delegation grants. Roles are admin-managed; the platform role SUPER_ADMIN is the only role that cannot be self-assigned. Org admins can shrink their org's capability set; they cannot expand it beyond what their org type (ORIGINATOR / INVESTOR / CORRESPONDENT / SUBSERVICER) permits.
Cross-tenant operations record actingAsOrgId on every audit row, so a wholesale lender can prove which originator instigated each request under whose delegation grant. Administrative tools that operate against another tenant set the X-Owner-Override header. Every override writes an audit row with action OWNER_OVERRIDE, the operator's key id, the target owner, and the request URI. Operators cannot disable the audit row.
Audit chain
common_audit_log is append-only. Each row carriesprevious_hash, entry_hash, and acting_as_org_id; entry_hash = SHA-256(previous_hash || canonical(row)) where canonical(row) is a deterministic JSON serialization that includes acting_as_org_id. Mutating a row breaks the chain at the next row. The verify endpoint /v1/admin/audit/verify walks the chain and reports the first break (or ok: true if the chain is intact). When a delegation grant is involved, acting_as_org_id equals the grantor org id while actor.org_id equals the grantee — wholesale and TPO audit evidence is unambiguous.
AI assistant prompts and replies write to their own audit-chained log with operator id, scope, tokens consumed, and citation correlationIds. The same verify endpoint covers the AI log; an unresolved citation produces an audit warning rather than a silent pass.
PII redaction
PiiRedactor is a pre-write filter that scrubs emails, phone numbers, SSN-shaped sequences, and credit-card-shaped sequences from log and audit payloads. Redaction runs before persistence; we do not rely on post-process redaction in downstream log storage.
SSRF defense
SafeUrlValidator blocks DNS-resolved IPs in the loopback, private (10/8, 172.16/12, 192.168/16), link-local (169.254/16), and cloud-metadata (169.254.169.254) ranges. Per-service host allowlists further restrict outbound endpoints — the scraper-service, webhook-service, and email-service each maintain their own. Out-of-allowlist hosts are rejected with a structured error.
Distributed rate-limit & lockout
API key brute-force lockout uses bucket4j on Redis: one bucket per key, consumed on every authentication failure, refilled on success. Lockout decisions are global across api-service replicas; an attacker cannot evade the lockout by hitting different replicas.