Showing all phase content; selected phase is highlighted in section badges.

Security Architecture

As-of: 2026-05-09. Source: passkey-shell backend 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 jose library 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 oid claim maps to User.entraObjectId in 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:

  1. Middleware: auth.middleware.ts exports requireAuth (resolves identity, hydrates permissions) and requireRole(...roles) (guards route access). Admin routes (admin.routes.ts) use requireRole('ADMIN').

  2. 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 on GovernedResourceAuthority mappings.

Permission Inheritance via Vault Folders

Permissions are resolved from Keeper’s shared-folder model:

  • VaultFolder is the ACL container. Records inherit their folder’s ACL.
  • VaultFolderPermission maps principals (user emails, Entra group IDs) to roles (OWNER, MANAGER, EDITOR, VIEWER).
  • OWNER/MANAGER can approve; EDITOR/VIEWER can request.
  • permission.service.ts resolves effective permissions per user, combining Entra group memberships with Vault folder ACLs.
  • Results cached in UserPermissionCache with 5-minute TTL, invalidated on Vault permission changes by permission-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() rejects shareLinkHash values that match ^https?:\/\/ — a defense-in-depth check that prevents a coding error from persisting a URL-shaped string.
  • Verification: approve-then-issue.test.ts explicitly 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#issue where the token exchange happens.
  • Log redaction: log-redaction.middleware.ts is installed early in server.ts (immediately after dotenv/config), before any application imports that might log at load time. It monkey-patches console.log, console.error, console.warn, console.info to 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 the Request row. The plaintext is returned once to the caller and never stored server-side.
  • Verification: crypto.timingSafeEqual for constant-time comparison — prevents timing side-channel attacks.
  • Rate limiting: issuanceCount / maxIssuances (default 3) per request. Checked inside a Prisma $transaction so concurrent calls see committed state.

Audit Trail

What’s Persisted

Two audit surfaces:

  1. 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.

  2. 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).

Retention and Integrity

  • Append-only: Both AuditEvent and GovernanceDecisionTrace tables are insert-only from the application layer. No UPDATE or DELETE operations exist in the codebase.
  • Authority history: GovernedResourceAuthority rows are never deleted — revoked rows retain full history with revokedAt, 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: inputsHash on 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

LayerEncryptionKey Management
Postgres Flexible ServerAzure-managed encryption at rest (AES-256)Microsoft-managed keys (default). Customer-managed keys available if required.
Azure Key VaultFIPS 140-2 Level 2 validated HSMsVault-managed. Secrets, keys, certificates encrypted at rest.
Keeper VaultKeeper’s zero-knowledge encryption (AES-256, RSA-2048)User-managed master password + organizational key. Anthropic/application never sees plaintext vault contents.
App ServiceAzure Storage encryption at restMicrosoft-managed. Applies to deployed artifacts.

In Transit

ChannelProtocolNotes
Client ↔ App ServiceTLS 1.2+Azure-managed certificate on *.azurewebsites.net. Custom domain needs separate cert.
App Service ↔ PostgresTLS 1.2+Enforced by Postgres Flexible Server SSL configuration.
App Service ↔ Key VaultTLS 1.2+Managed identity authentication, no secrets in transit.
App Service ↔ KSMTLS 1.2+KSM SDK handles transport encryption.
App Service ↔ CommanderLocal subprocessCommander runs on the same host — no network transit for the subprocess call. Commander’s API calls to Keeper use TLS.
App Service ↔ EntraTLS 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_IDS restricts JWT validation to the configured tenant.
  • ENTRA_ALLOWED_ISSUERS restricts 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

ThreatMitigationResidual Risk
Stolen JWTShort-lived tokens (Entra default 1hr), stateless validation, no refresh token persistence server-sideToken replay within TTL window
Commander subprocess compromiseSubprocess runs with least-privilege, configurable timeout, error classificationCommander has user-session-level access to vault
Share URL interception in transitTLS on all channels, URL never persisted (INV-5), log redactionTransit interception between App Service and client (TLS mitigates)
Database compromiseKey Vault references (no raw secrets in app config), Postgres encryption at restIf KV access is compromised, DATABASE_URL leaks
Stale permission cache5-minute TTL, invalidation on vault permission changesUp to 5 minutes of stale permissions after a change
Authority history tamperingINV-1 BEFORE UPDATE trigger, append-only modelTrigger can be dropped by DB admin with DDL access

v3 Deltas

ThreatMitigationResidual Risk
Commander in full mode (live shares)Error classification, retry budget, UNFULFILLABLE fallbackIf Commander credentials leak, attacker can create shares
Bot Framework (Teams notifications)Bot identity separate from user identity, Adaptive Cards don’t embed secretsBot could be impersonated if bot registration credentials leak
HARDEN_GOVERNANCE_v1=trueFull token-exchange enforcementBreaking change if untested — must validate in staging first

v4 SMS Surface

ThreatMitigationResidual Risk
SIM swapDevice-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-useSS7 vulnerabilities are infrastructure-level; application can’t fully mitigate
OTP replaySingle-use tokens, challenge-bound, short TTLWindow between send and expiry
Android device compromiseIf companion app: app-level encryption, biometric guardIf standard SMS: no device-level protection
SMS gateway compromiseTLS to gateway, API key in Key VaultIf 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 FamilyProduct BehaviorCode Reference
CC6.1 — Logical accessRole-based access (REQUESTER/APPROVER/ADMIN), Entra ID SSO, policy engine enforcementauth.middleware.ts, policy/engine.ts
CC6.2 — Access provisioningAuthority mappings, permission cache with TTL, vault folder ACL resolutionauthority.service.ts, permission.service.ts
CC6.3 — Access removalLease expiry, permission revocation, authority revocation (append-only history)lease.service.ts, revocation.service.ts, authority.service.ts
CC7.2 — Monitoring27-action audit trail, governance decision traces with reproducibility hashesaudit.service.ts, GovernanceDecisionTrace model
CC7.3 — Anomaly detectionIssuance rate limiting (maxIssuances cap), token mismatch audit eventsissuance.service.ts, ISSUANCE_TOKEN_MISMATCH event
CC8.1 — Change managementPolicy version stamping, evaluating code version (git SHA), inputs hash for replaypolicy/version.ts, GovernanceDecisionTrace.policyVersion
HITRUST 01.b — User registrationEntra ID provisioning, no local password storeidentity.service.ts
HITRUST 09.aa — Audit loggingAppend-only audit events, governance probe events, telemetryAuditEvent, GovernanceProbeEvent, telemetry.service.ts
HITRUST 10.a — Cryptographic controlsAES-256 at rest (Postgres, KV), TLS 1.2+ in transit, SHA-256 hashing (INV-5), constant-time token verificationSchema, Key Vault config, issuance.service.ts