Security Architecture
As-of: 2026-05-09. Source:
passkey-shellbackend source, middleware, integration tests, deploy configuration.Compliance posture: Designed to align with SOC 2 Type II and HITRUST CSF control families. Not certified. No certification is claimed.
Identity and Authentication
Entra ID Integration
Authentication is Entra ID (Azure AD) via JWT validation. The backend does not maintain its own user/password store.
Implementation: identity.service.ts (EntraIdentityService)
- JWT verification uses
joselibrary against remote JWKS endpoint. - Algorithm: RS256.
- Validates:
aud(audience),iss(issuer),tid(tenant ID),oid(object ID),exp(expiry). - Configuration via env vars:
ENTRA_TENANT_ID,ENTRA_AUDIENCE,ENTRA_ALLOWED_TENANT_IDS,ENTRA_JWKS_URI,ENTRA_ALLOWED_ISSUERS. - Staging: audience =
api://9b686ae7-f387-441c-a5a7-fea22e1ba126, tenant =92023f7c-4825-450c-bb41-db93ab279f44. - User lookup: JWT
oidclaim maps toUser.entraObjectIdin Postgres.
Session model: Stateless JWT validation per request. No server-side sessions. Token refresh is client-side (MSAL in the React frontend). Group memberships are cached server-side for 5 minutes in UserPermissionCache.
Error handling: Custom error classes (IdentityConfigurationError, IdentityValidationError, IdentityProvisioningError) provide typed failure modes. getIdentityConfigurationDiagnostics() is exposed on the admin health endpoint for operators to verify configuration without leaking secrets.
v3 delta
No changes to identity model. Bot Framework registration will add a separate identity for the bot (M365 app registration), but user authentication remains Entra JWT.
v4 delta
SMS OTP adds a second authentication factor to credential retrieval only (not to portal login). The identity model stays Entra-first.
Authorization
Role-Based Access Control
Three roles: REQUESTER, APPROVER, ADMIN. Enforced at two layers:
-
Middleware:
auth.middleware.tsexportsrequireAuth(resolves identity, hydrates permissions) andrequireRole(...roles)(guards route access). Admin routes (admin.routes.ts) userequireRole('ADMIN'). -
Policy engine: The pure-function policy evaluator (
policy/engine.ts) applies rules that enforce authorization decisions:self-approval-block— Owners cannot approve their own resources.sensitivity-escalation— High-sensitivity records require higher authority.authority-routing— Routes approval to the designated authority (OWNER, CUSTODIAN, APPROVER) based onGovernedResourceAuthoritymappings.
Permission Inheritance via Vault Folders
Permissions are resolved from Keeper’s shared-folder model:
VaultFolderis the ACL container. Records inherit their folder’s ACL.VaultFolderPermissionmaps principals (user emails, Entra group IDs) to roles (OWNER, MANAGER, EDITOR, VIEWER).- OWNER/MANAGER can approve; EDITOR/VIEWER can request.
permission.service.tsresolves effective permissions per user, combining Entra group memberships with Vault folder ACLs.- Results cached in
UserPermissionCachewith 5-minute TTL, invalidated on Vault permission changes bypermission-sync.job.ts.
Authority Model
GovernedResourceAuthority rows define who has what authority over each governed resource:
- OWNER: The record’s owner in Postgres.
- CUSTODIAN: The record’s owner in the Vault (may differ from Postgres owner).
- APPROVER: Designated approver — can be a specific user, an Entra group, a role, or an email.
Authority rows are append-only (INV-1). Active rows cannot be updated — only revoked (setting revokedAt) or replaced (revoke predecessor + insert successor with supersededById linkage). Immutability is enforced by a Postgres BEFORE UPDATE trigger installed by migration 202604240000_harden_governance_authority_additive.
Secret Handling
No Raw Credentials in Application
All secrets are Azure Key Vault references. The application never sees raw database passwords, KSM tokens, or Entra secrets:
DATABASE_URL→@Microsoft.KeyVault(SecretUri=https://kv-pk-stg-ben-6b2f.vault.azure.net/secrets/DATABASE-URL/)KSM_CONFIG→@Microsoft.KeyVault(SecretUri=https://kv-pk-stg-ben-6b2f.vault.azure.net/secrets/KSM-CONFIG/)ENTRA_TENANT_ID→@Microsoft.KeyVault(SecretUri=...)
Key Vault uses RBAC mode (not access policies). The App Service’s managed identity has Key Vault Secrets User role.
Share URLs Never Persisted (INV-5)
The one-time share URL returned by Commander is treated as transit-only data:
- Storage: Only the SHA-256 hash is stored (
IssuanceEvent.shareLinkHash). The URL itself is returned to the requester in the HTTP response body and never written to the database. - Guard:
issuance.service.createIssuanceEvent()rejectsshareLinkHashvalues that match^https?:\/\/— a defense-in-depth check that prevents a coding error from persisting a URL-shaped string. - Verification:
approve-then-issue.test.tsexplicitly asserts no URLs exist in the database after an issuance flow. - Notifications:
notification.service.sendShareLink()no longer embeds the URL in the notification body. Instead, it sends a deep link to/requests/:id#issuewhere the token exchange happens. - Log redaction:
log-redaction.middleware.tsis installed early inserver.ts(immediately afterdotenv/config), before any application imports that might log at load time. It monkey-patchesconsole.log,console.error,console.warn,console.infoto scrub share URLs from output using regex replacement.
Issuance Token Security
The ephemeral issuance token (used for credential retrieval after approval) is cryptographically generated and handled:
- Generation:
crypto.randomBytes(32)→ 64-char hex string (256 bits entropy). - Storage: Only the SHA-256 hash (
issuanceTokenHash) is persisted on theRequestrow. The plaintext is returned once to the caller and never stored server-side. - Verification:
crypto.timingSafeEqualfor constant-time comparison — prevents timing side-channel attacks. - Rate limiting:
issuanceCount/maxIssuances(default 3) per request. Checked inside a Prisma$transactionso concurrent calls see committed state.
Audit Trail
What’s Persisted
Two audit surfaces:
-
AuditEvent— 40-action enum covering:- Request lifecycle:
REQUEST_CREATED,REQUEST_APPROVED,REQUEST_DENIED - Lease lifecycle:
LEASE_STARTED,SHARE_ISSUED,LEASE_RELEASED,LEASE_EXPIRED,LEASE_REVOKED_PERMISSION_LOST - Vault sync:
CREDENTIAL_ROTATED,RECORD_ORPHANED,VAULT_SYNC_DETECTED_CHANGES - Discovery:
VAULT_DISCOVERY_STARTED,VAULT_DISCOVERY_COMPLETED,RECORD_AUTO_DISCOVERED - Issuance:
ISSUANCE_TOKEN_MISMATCH,ISSUANCE_RATE_LIMITED,ISSUANCE_VAULT_FAIL,ISSUANCE_SUCCEEDED - Permissions:
USER_PERMISSIONS_HYDRATED,ACCESS_DENIED_INSUFFICIENT_PERMISSION - Commander:
COMMANDER_ROTATION_DUE
Each event records: actor (user ID), action, request ID (if applicable), detail string, timestamp.
- Request lifecycle:
-
GovernanceDecisionTrace— Full decision audit per governance evaluation:- Stage:
REQUEST_SUBMISSION,AUTHORITY_RESOLUTION,APPROVAL_DECISION,ISSUANCE_CHECK,ISSUANCE_EVENT,REVOCATION_EVENT. - Outcome:
ALLOW,DENY,PENDING. - Structured JSON:
reasonsJson,constraintsJson,requiredApprovalsJson,traceJson. - Reproducibility fields:
policyVersion,evaluatedByVersion(git SHA),inputsHash(SHA-256 of canonicalized inputs).
- Stage:
Retention and Integrity
- Append-only: Both
AuditEventandGovernanceDecisionTracetables are insert-only from the application layer. No UPDATE or DELETE operations exist in the codebase. - Authority history:
GovernedResourceAuthorityrows are never deleted — revoked rows retain full history withrevokedAt,revokedBy,revokedReason. INV-1 enforced by DB trigger. - Retention policy: Not yet configured. Rows accumulate indefinitely. v3 should define retention windows and archival strategy.
- Integrity verification:
inputsHashon decision traces enables replay verification — given the same inputs, the policy engine should produce the same decision.
Governance probe events
GovernanceProbeEvent records every probe execution: who invoked it, when, exit code, freshness (fresh vs. cached), degraded flag, duration. Indexed by invokedAt and commandId + invokedAt for query patterns.
Encryption Posture
At Rest
| Layer | Encryption | Key Management |
|---|---|---|
| Postgres Flexible Server | Azure-managed encryption at rest (AES-256) | Microsoft-managed keys (default). Customer-managed keys available if required. |
| Azure Key Vault | FIPS 140-2 Level 2 validated HSMs | Vault-managed. Secrets, keys, certificates encrypted at rest. |
| Keeper Vault | Keeper’s zero-knowledge encryption (AES-256, RSA-2048) | User-managed master password + organizational key. Anthropic/application never sees plaintext vault contents. |
| App Service | Azure Storage encryption at rest | Microsoft-managed. Applies to deployed artifacts. |
In Transit
| Channel | Protocol | Notes |
|---|---|---|
| Client ↔ App Service | TLS 1.2+ | Azure-managed certificate on *.azurewebsites.net. Custom domain needs separate cert. |
| App Service ↔ Postgres | TLS 1.2+ | Enforced by Postgres Flexible Server SSL configuration. |
| App Service ↔ Key Vault | TLS 1.2+ | Managed identity authentication, no secrets in transit. |
| App Service ↔ KSM | TLS 1.2+ | KSM SDK handles transport encryption. |
| App Service ↔ Commander | Local subprocess | Commander runs on the same host — no network transit for the subprocess call. Commander’s API calls to Keeper use TLS. |
| App Service ↔ Entra | TLS 1.2+ | JWKS fetch and token validation. |
Tenant Isolation
Current Model (v1 — Single Tenant)
- One Entra tenant (
92023f7c-...). - One Keeper vault per deployment.
- One KSM Application per environment (staging, production).
ENTRA_ALLOWED_TENANT_IDSrestricts JWT validation to the configured tenant.ENTRA_ALLOWED_ISSUERSrestricts accepted issuers.
Multi-Tenant Considerations (v3+)
Not designed for multi-tenant. If multi-tenant is a v3/v4 goal, the following would need to change:
- Tenant-scoped data partitioning in Postgres.
- Per-tenant KSM Application and Commander configuration.
- Tenant ID claim enforcement in the policy engine.
- Separate Key Vault per tenant (or strict access policy segmentation).
Threat Surface by Phase
v1 Baseline
| Threat | Mitigation | Residual Risk |
|---|---|---|
| Stolen JWT | Short-lived tokens (Entra default 1hr), stateless validation, no refresh token persistence server-side | Token replay within TTL window |
| Commander subprocess compromise | Subprocess runs with least-privilege, configurable timeout, error classification | Commander has user-session-level access to vault |
| Share URL interception in transit | TLS on all channels, URL never persisted (INV-5), log redaction | Transit interception between App Service and client (TLS mitigates) |
| Database compromise | Key Vault references (no raw secrets in app config), Postgres encryption at rest | If KV access is compromised, DATABASE_URL leaks |
| Stale permission cache | 5-minute TTL, invalidation on vault permission changes | Up to 5 minutes of stale permissions after a change |
| Authority history tampering | INV-1 BEFORE UPDATE trigger, append-only model | Trigger can be dropped by DB admin with DDL access |
v3 Deltas
| Threat | Mitigation | Residual Risk |
|---|---|---|
| Commander in full mode (live shares) | Error classification, retry budget, UNFULFILLABLE fallback | If Commander credentials leak, attacker can create shares |
| Bot Framework (Teams notifications) | Bot identity separate from user identity, Adaptive Cards don’t embed secrets | Bot could be impersonated if bot registration credentials leak |
HARDEN_GOVERNANCE_v1=true | Full token-exchange enforcement | Breaking change if untested — must validate in staging first |
v4 SMS Surface
| Threat | Mitigation | Residual Risk |
|---|---|---|
| SIM swap | Device-bound MFA on top of SMS (v4+ hardening) | SMS alone is insufficient against SIM swap — this is a known limitation |
| SMS interception (SS7) | Short OTP TTL, single-use | SS7 vulnerabilities are infrastructure-level; application can’t fully mitigate |
| OTP replay | Single-use tokens, challenge-bound, short TTL | Window between send and expiry |
| Android device compromise | If companion app: app-level encryption, biometric guard | If standard SMS: no device-level protection |
| SMS gateway compromise | TLS to gateway, API key in Key Vault | If gateway is compromised, OTPs leak in transit |
Compliance Control Mapping
Designed to align with SOC 2 Type II Trust Services Criteria and HITRUST CSF v11. Not certified.
| Control Family | Product Behavior | Code Reference |
|---|---|---|
| CC6.1 — Logical access | Role-based access (REQUESTER/APPROVER/ADMIN), Entra ID SSO, policy engine enforcement | auth.middleware.ts, policy/engine.ts |
| CC6.2 — Access provisioning | Authority mappings, permission cache with TTL, vault folder ACL resolution | authority.service.ts, permission.service.ts |
| CC6.3 — Access removal | Lease expiry, permission revocation, authority revocation (append-only history) | lease.service.ts, revocation.service.ts, authority.service.ts |
| CC7.2 — Monitoring | 27-action audit trail, governance decision traces with reproducibility hashes | audit.service.ts, GovernanceDecisionTrace model |
| CC7.3 — Anomaly detection | Issuance rate limiting (maxIssuances cap), token mismatch audit events | issuance.service.ts, ISSUANCE_TOKEN_MISMATCH event |
| CC8.1 — Change management | Policy version stamping, evaluating code version (git SHA), inputs hash for replay | policy/version.ts, GovernanceDecisionTrace.policyVersion |
| HITRUST 01.b — User registration | Entra ID provisioning, no local password store | identity.service.ts |
| HITRUST 09.aa — Audit logging | Append-only audit events, governance probe events, telemetry | AuditEvent, GovernanceProbeEvent, telemetry.service.ts |
| HITRUST 10.a — Cryptographic controls | AES-256 at rest (Postgres, KV), TLS 1.2+ in transit, SHA-256 hashing (INV-5), constant-time token verification | Schema, Key Vault config, issuance.service.ts |