Security controls
Technical and organisational measures grouped by category - identity & access, network, data protection, audit, resilience, software supply chain.
This page lists every control we rely on to satisfy GDPR Art. 32, NHS DSPT, and DCB0129. Each group ends with a pointer to the evidence location in the repository.
Identity and access
- Authentication - JWT-based with 24-hour access token TTL + 7-day refresh token TTL. Refresh rotation: using a refresh token invalidates it.
- Session revocation - Logout writes the token ID to a Redis denylist. Subsequent requests with that token are rejected.
- Password policy - 14+ characters, must contain uppercase, lowercase, digit, and special character. Bcrypt cost factor 12.
- Account lockout - 5 failed logins in 15 minutes triggers a 15-minute lockout.
- MFA - TOTP (RFC 6238) available for all users, enforced for admin and super_admin roles.
- RBAC - Every endpoint declares the permission it requires. Permissions are assigned per-clinic.
- Row-level security - PostgreSQL RLS enforces clinic isolation at the database level. Even if application code has a bug, the database refuses to return another clinic's data.
Evidence: backend/internal/auth/, backend/internal/middleware/auth.go, migration 000064.
Network and infrastructure
- All resources in Azure UK South, single VNet
hihuman-vnet - PostgreSQL and Redis have public network access disabled - reachable only via private endpoints inside the VNet
- Container App runs inside the VNet with managed identity
- Azure Front Door with OWASP 3.2 managed ruleset + Bot Manager + rate limiting (1000 req / 5 min per IP)
- TLS 1.2 minimum enforced on every endpoint
- Distroless container image: no shell, no package manager, cannot be exec'd into
- Weekly Trivy vulnerability scans on the container image with HIGH / CRITICAL thresholds
Evidence: infra/modules/network.bicep, infra/modules/postgres.bicep, infra/modules/frontdoor.bicep.
Data protection
- AES-256-GCM encryption for PII fields at rest (names, NHS numbers, phones, addresses)
- Full-disk encryption on Azure-managed disks
- TLS 1.2+ in transit everywhere
- PII regex scrubber on all outbound LLM and webhook traffic
- HMAC-SHA256 hashing for phone numbers in audit logs (reversible only with the HMAC key)
- Key rotation: AES key and JWT secret rotatable at any time without downtime
Evidence: backend/pkg/crypto/, backend/pkg/pii/, backend/internal/keyrotation/.
Audit and monitoring
- Every authenticated request creates an audit record with user ID, clinic ID, method, path, IP, timestamp
- Every mutating clinical action (create, update, delete on patients, appointments, consultations, prescriptions) is explicitly audited with the resource ID and change summary
- Audit logs exported daily to Azure immutable blob storage with a SHA-256 hash chain (each record includes a hash of the previous record)
- 7-year retention on all audit data
- Application telemetry to Azure Application Insights (trace IDs, exceptions, dependency tracking)
- Log Analytics queries for anomaly detection (unusual access patterns, failed login spikes)
Evidence: backend/internal/auditlog/, backend/internal/middleware/audit.go.
Resilience and recovery
- PostgreSQL: 35-day point-in-time recovery with geo-redundant backups (stored in UK West)
- Daily automated backup verification
- Zone-redundant high availability on production Postgres
- Container App auto-scales between 1 and 3 replicas based on HTTP concurrency
- Graceful shutdown with 30-second request drain window
- Circuit breakers on every external HTTP client prevent cascading failures
Evidence: backend/internal/backupverify/, backend/pkg/circuitbreaker/.
Software supply chain
- Dependency vulnerability scanning on every commit (govulncheck, gosec, staticcheck)
- Container image vulnerability scanning (Trivy, HIGH / CRITICAL fail the build)
- SBOM (Software Bill of Materials) generated in SPDX format for every release
- Signed commits enforced on main branch
- Branch protection requires PR review
Evidence: .github/workflows/ci.yml.