Skip to main content

Infrastructure security requirements

Why this document exists

Some controls cannot be implemented in this repository. At-rest encryption for the Postgres data directory, encrypted backups, HSM key custody and ingress TLS live in the external Postgres cluster and in the ehr/deployment Helm chart. Writing a backend ticket for them produces a ticket that can never close.

So this repo owns two things instead: the requirement, and the conformance test that makes it verifiable.

⚠ That distinction is the point. "The database uses TDE" is a claim. "This test fails when the connection is not TLS" is a control. A compliance matrix (M33-016) can cite the second and cannot honestly cite the first.

The controls

#ControlOwning repoConformance check
1Postgres data directory and WAL encrypted at restcluster / infra⚠ none possible from here — see What this cannot verify
2App→database connections use TLSehr/deployment (DB_SSL_MODE)databaseConnectionIsEncryptedInTransitpg_stat_ssl.ssl for the app's own backend
3Ingress serves TLS with a chain that validatesehr/deployment (ingress.tls)ingressServesValidTls — a default JDK client, so a self-signed certificate fails
4X-Forwarded-Proto: https reaches the applicationehr/deployment (ingress)hstsIsServedOverHttps — without it M33-001's HSTS never fires
5Plain HTTP redirects rather than serving the APIehr/deployment (ingress)plainHttpIsNotServed
6Backups encrypted, separately keyed, restore testedcluster / infra⚠ none possible from here
7PII key custody in an HSM/KMSehr/deployment (PII_KEY_PROVIDER)partially — the app refuses to start with vault and no wrapped keys
8POSTGRES_PASSWORD from a secret, never a literalthis repo + ehr/deploymentalready done — see below

⚠ Control 8 was already fixed; the ticket text was stale

M33-015's body asks for "POSTGRES_PASSWORD from a secret rather than the literal change-me". M33-003 already did this. docker-compose.yml now reads:

POSTGRES_PASSWORD: ${DB_PASSWORD:?Set DB_PASSWORD in .env — there is deliberately no default}

Recorded here rather than silently skipped, because a control listed as outstanding when it is done is how a matrix ends up understating what exists — the mirror of overstating it.

The conformance test

core/src/test/java/…/conformance/DeployedEnvironmentSecurityConformanceTest.java

⚠ The security:conformance CI job runs only on a deliberately-started pipeline (Run pipeline, or a schedule) — never on an MR. A when: manual job with allow_failure: false is a blocking manual job: GitLab holds the pipeline in manual instead of success until someone runs it, which on an MR reads as "not green" and can stop the merge. Where it does run, allow_failure: false stands.

CONFORMANCE_BASE_URL=https://uhp.zhenus.local \
DB_HOST=… DB_PORT=… DB_NAME=… DB_USERNAME=… DB_PASSWORD=… \
./mvnw -pl core test -Dsurefire.excluded.groups= -Dgroups=conformance

⚠⚠ It fails rather than skips when unconfigured. A conformance check that quietly skips reports green while verifying nothing, which is worse than having no check: it manufactures evidence of compliance where none was gathered. If it runs, it either verified the control or it failed.

Tagged conformance, excluded in two places. The root pom's surefire.excluded.groups and the explicit override test:unit passes in .gitlab-ci.yml — that override replaces the default rather than adding to it, so excluding it in only one place would let it run somewhere nobody intended, against nothing.

⚠ The HTTPS client is a default HttpClient, which validates the chain against the JDK trust store. A permissive client would turn control 3 into "TLS with any chain", which is exactly what a forgotten self-signed certificate satisfies.

Baseline: what fails today (2026-08-28)

Run against the deployed environment, all four checks fail, and each failure names a real gap:

CheckResultEvidence
ingressServesValidTlsSSLHandshakeException: PKIX path building failed
hstsIsServedOverHttpssame handshake failure — HSTS cannot be reached to be checked
plainHttpIsNotServedplain HTTP returns 200; the API is served in the clear
databaseConnectionIsEncryptedInTransitThe server does not support SSL (local dev Postgres)

Corroborated outside the test: kubectl -n uhp get ingress reports PORTS 80, and the ingress spec.tls is empty — matching values.yaml's tls: [].

This baseline is the deliverable, not a defect report. M33-015 is complete when the requirement and the check exist; the checks going green is ehr/deployment's work, tracked separately.

What this cannot verify, and why saying so matters

Controls 1 and 6 — data-directory encryption and backup encryption — cannot be checked from an application connection. Postgres exposes no reliable way to ask whether its storage is encrypted; pg_stat_ssl describes the wire, not the disk.

⚠ So this repo must not assert that TDE "is done". Any such claim in a compliance matrix would be unfounded, and the honest entry is: control owned by infra, evidenced by an infrastructure attestation (disk/volume encryption configuration, backup tooling configuration and a dated restore test), not by anything in this codebase.

Cross-repo tracking

The Helm changes these checks require — populating ingress.tls, keeping DB_SSL_MODE, and the PII_* custody settings M33-003 needs — are tracked in ehr/deployment#2. ⚠ Note for whoever picks that up: values.yaml is the file CI rewrites and the one ArgoCD deploys. values-prod.yaml is linted by CI and deployed by nothing (see the note on #336), so adding a tls: block only there would change nothing while looking like it had.