Skip to main content

CC6.1 — Logical access security software, infrastructure, and architectures

Status: Partially implemented (tenant-auth + JWT live; RS256 + JWKS migration tracked sprint 2) Owner: Agent #38 (Senior Compliance Lead, SOC 2 + ISO 27001) Last reviewed: 2026-05-28 Next review: 2026-08-28

Trust Services Criteria reference

The entity implements logical access security software, infrastructure, and architectures over protected information assets to protect them from security events to meet the entity's objectives. The control covers the access-control technology stack, the placement of access checks at every entry point, the credential lifecycle, and the cryptographic protection of credentials in transit and at rest.

How ZeroAuth meets this control

The logical-access surface is partitioned by audience and gated independently.

Tenant API surface (/v1/*). Authentication is by API key in the Authorization: Bearer za_{live,test}_{48 hex} header. Keys are issued via the developer console (/api/console/keys), stored SHA-256-hashed at rest, and scope-checked per endpoint per src/types/. The authenticateTenantApiKey middleware in src/middleware/tenant-auth.ts is required on every /v1/* route — enforced by the source-level guard tests/tenant-isolation.test.ts (commit a1bbc47). 14 public-route exceptions are explicitly enumerated in PUBLIC_ROUTE_EXCEPTIONS with ≥ 20-character justifications. A developer who tries to land an unauthenticated /v1/* endpoint trips the test before merge.

Console surface (/api/console/*). Authentication is JWT-based. The token is delivered as an HttpOnly cookie zeroauth_console_jwt scoped to /api/console, replacing the prior ?access_token=<jwt> query fallback that lands JWTs in Caddy access logs (audit finding C-3 closure, commit ee6aad4). The replacement test is tests/console-auth.test.ts::"P0 audit finding C-3". Per-IP rate limit on signup + login lives at src/routes/console.ts:authLimiter — 10 attempts per 15 minutes per IP, with a stricter password policy (12 chars, letter+digit, denylist of common passwords) per threat-model row A-05. JWTs today are HS256; RS256 migration with a JWKS endpoint is tracked as audit finding C-11 open-sprint-2.

Admin surface (/api/admin/*). Single shared x-api-key in .env, gated by src/middleware/auth.ts. Read-only — admin actions that write must go through the console + audit chain. Compliance roadmap names the JWT-RS256 migration + per-admin key rotation as a sprint-2 deliverable.

Public surfaces. /api/health (unauthenticated subsystem-status only), /api/leads/* (marketing forms; writes only to the leads table), /api/auth/saml/* + /api/auth/oidc/* (demo stubs gated by ENABLE_DEMO_AUTH=true; threat-model rows A-03 + A-04). Demo gates default off in production; src/middleware/demo-auth-gate.ts returns 503 unless explicitly enabled.

Cryptographic protection. TLS terminates at Caddy (Caddyfile at repo root). The VPS at 104.207.143.14 accepts SSH only on port 22 with key authentication (root laptop key + zeroauth-deploy CI key per threat-model surface inventory). Postgres binds to localhost only inside the docker network. Secrets live in /opt/zeroauth/.env on the VPS and never enter the repo (.env, PRODUCTION_CREDENTIALS.md, GITHUB_SECRETS.md are gitignored).

Audit trail of access. Every authenticated action writes an audit_events row through appendAuditEvent; the SHA-256 chain (ADR 0013, commit 27ed93c + commit a475ed8) makes the trail tamper-evident; the daily on-chain anchor (ADR 0014, commit 27ed93c + commit d6c6a4e) makes it externally verifiable.

Cross-tenant logical isolation. Beyond the per-route middleware guard, every SQL query in src/services/platform.ts (and similar) takes (tenant_id, environment) as parameters and embeds them in the WHERE clause. Schema-purity test tests/schema-purity.test.ts (commit 5425032) locks down the tenant-scoped table columns so a new PII column cannot sneak in without review. Threat-model row A-01.

Service-to-service auth. The on-chain anchor job (per ADR 0014) uses a deployer wallet to call AuditAnchor.anchor(). The wallet is the single onlyOwner on DIDRegistry per the threat-model surface inventory; rotation is via npm run wallet:rotate.

Evidence references

  • src/middleware/tenant-auth.ts — tenant API key middleware (path verified in worktree).
  • src/middleware/auth.ts — admin x-api-key middleware.
  • src/middleware/demo-auth-gate.ts — demo-auth gating.
  • Commit a1bbc47 — source-level cross-tenant guard tests/tenant-isolation.test.ts.
  • Commit ee6aad4?access_token= query fallback removal (C-3 closure).
  • Commit 5425032 — schema-purity test landing tenant-scoped column lock.
  • Commit a475ed8 — audit hash chain.
  • Commit d634b2d/api/admin/audit-integrity endpoint.
  • Commit d6c6a4eAuditAnchor contract on-chain anchor sink.
  • ADR 0013-audit-log-hash-chain.md (commit 27ed93c).
  • ADR 0014-on-chain-anchor-cadence.md (commit 27ed93c).
  • docs/threat_model.md rows A-01, A-03, A-04, A-05 — access-control threat surfaces.
  • Caddyfile — TLS termination.

Open gaps + remediation roadmap

  • RS256 JWT + JWKS publication — audit finding C-11 open-sprint-2; rollover playbook lands docs/operations/jwt-key-rotation-playbook.md.
  • Postgres-backed session store — audit finding C-9 open-sprint-2; replaces in-memory store, enables horizontal scale-out.
  • Postgres-backed rate-limit middleware — audit finding C-10 open-sprint-2.
  • Per-tenant CORS allowlist — audit finding C-13 open-sprint-2; replaces wildcard CORS.

Test or audit query

cat src/middleware/tenant-auth.ts shows the middleware exists. grep -E "router\\.(get|post|put|patch|delete)" src/routes/v1/*.ts | wc -l returns the route count; the same against tests/tenant-isolation.test.ts::"PUBLIC_ROUTE_EXCEPTIONS" should show ≤ 14 public exceptions. git log --oneline -- src/middleware/ proves the middleware file is in version control.