Context
This is a Minimum Viable Demo (MVD) of an EHDS-compliant health dataspace. The stack is intentionally run in dev-mode (no TLS, hardcoded secrets, in-memory Vault) so it can be deployed with a single docker compose up. The security assessment has two distinct tracks:
| Track |
Goal |
When |
| 🟢 Demo / MVD |
Prove the architecture is sound; fix real code-level risks; document the threat model |
Now — this issue |
| 🔵 Production |
Full infrastructure hardening for a live deployment; BSI C5 audit |
Future milestone |
The demo track does not require replacing Vault dev-mode or adding TLS — those are documented as known dev-only shortcuts. It does require fixing injection risks, proving auth works correctly, and producing the BSI C5 gap analysis that shows what would change in production.
Why BSI C5?
BSI C5 is the German Federal Office for Information Security's cloud security catalogue. Under EHDS Article 50(6), secondary-use health dataspaces must demonstrate conformance with a recognised security framework. BSI C5 is the applicable standard for EU health infrastructure. The demo proves we understand what needs to change and why — the production track delivers the actual changes.
🟢 Demo / MVD Phase — Implement Now
These are real security fixes that can and should be done in the demo codebase today. They demonstrate security maturity without requiring infrastructure changes.
D1 — Fix Cypher Injection Risk (CRITICAL for any phase)
File: ui/src/app/api/admin/audit/route.ts
Risk: The WHERE clause is built by string-escaping user input (replace(/'/g, "\\'")). This is insufficient — parameterised Cypher must be used.
BSI C5: DEV-07 (secure coding)
// ❌ Current (injection risk)
const where = `WHERE t.status = '${params.status?.replace(/'/g, "\\'")}'`
// ✅ Fix: parameterised Cypher
const result = await runQuery(
`MATCH (t:TransferEvent) WHERE t.status = $status RETURN t`,
{ status: params.status }
)
Demo value: Shows we understand and fix OWASP A03 (Injection) at the code level.
D2 — Add Content Security Policy Headers
File: ui/next.config.ts
Risk: No CSP → XSS can exfiltrate session tokens from the browser.
BSI C5: DEV-07, NC-03
const securityHeaders = [
{ key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval'; connect-src 'self' http://localhost:*" },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
]
Demo value: Shows headers are correct even in dev mode; easy to tighten in production.
D3 — Security E2E Test Suite (__tests__/e2e/security/)
Playwright tests that prove the auth and middleware actually work:
| Test ID |
What it proves |
| SEC-01 |
GET /admin without session → redirects to /auth/signin (not 200) |
| SEC-02 |
GET /compliance without session → redirects to /auth/signin |
| SEC-03 |
GET /patient/profile without session → redirects to /auth/signin |
| SEC-04 |
GET /api/admin/policies without session → 401 or redirect |
| SEC-05 |
GET /api/graph without session → 200 (public, not protected) |
| SEC-06 |
GET /catalog without session → 200 (public, not protected) |
| SEC-07 |
Response headers include X-Content-Type-Options: nosniff |
| SEC-08 |
Response headers include X-Frame-Options: DENY |
| SEC-09 |
localStorage on / does not contain NEXTAUTH_SECRET or any token |
| SEC-10 |
/auth/signin page does not expose Keycloak client secret in page source |
Demo value: These tests run in CI and prove that route protection is not accidentally disabled by a future refactor.
D4 — npm audit in CI (Zero High/Critical CVEs)
Add to .github/workflows/test.yml:
- name: Dependency vulnerability scan
run: |
cd ui && npm audit --audit-level=high
cd ../services/neo4j-proxy && npm audit --audit-level=high
Demo value: Shows the dependency supply chain is monitored. Any new CVE fails the build.
D5 — detect-secrets baseline (no secrets in repo)
pip install detect-secrets
detect-secrets scan --baseline .secrets.baseline
Commit .secrets.baseline so future commits are checked against it in pre-commit.
Demo value: Proves no real credentials are accidentally committed — .env.example values are clearly placeholders.
D6 — DSP / DCP Threat Model Document
File: docs/security/threat-model.md
Document the 7 protocol-level attack vectors for DSP and DCP with:
- Attack description
- Current mitigation in the demo stack (e.g. "EDC-V validates
@id idempotency")
- Production hardening required (e.g. "add request signing with participant private key")
Demo value: Shows evaluators that we have analysed the protocols — not just wired them up.
D7 — BSI C5 Gap Analysis Document
File: docs/security/bsi-c5-gap-analysis.md
For each of the 10 BSI C5 domains in scope:
- Current demo status: Compliant / Partial (dev shortcuts) / Not yet implemented
- What the demo proves: architecture is correct
- What production would add: TLS everywhere, real key management, etc.
Demo value: The gap analysis IS the deliverable for a demo — it shows we know what C5 requires and have a credible plan.
D8 — Rate Limiting on Admin API Routes
File: ui/src/middleware.ts
Add a simple in-memory rate limiter for /api/admin/* routes (e.g. max 60 req/min per IP).
Demo value: Shows awareness of API abuse risk; trivial to replace with Redis-backed limiter in production.
🔵 Production Phase — Document Now, Implement Later
These require infrastructure changes and are out of scope for the demo. They are documented here so a production team has a clear checklist.
Infrastructure Hardening (not needed for demo)
| # |
Finding |
Location |
BSI C5 |
Production fix |
| P1 |
Vault root token used by all services |
identityhub.env, issuerservice.env |
COS-04 |
AppRole per service with least-privilege policy |
| P2 |
Vault single key-share (-key-shares=1) |
vault-init-or-unseal.sh |
COS-04 |
Shamir 3-of-5, keys in HSM or cloud KMS |
| P3 |
Vault init.json with unseal key on disk |
vault-init-or-unseal.sh |
COS-04 |
Auto-unseal via AWS KMS / Azure Key Vault |
| P4 |
tls_disable = true in Vault |
vault.hcl |
NC-03 |
TLS with internal CA; mTLS between services |
| P5 |
sslRequired: none in Keycloak |
keycloak-realm.json |
NC-03 |
sslRequired: all; external TLS termination |
| P6 |
edc.iam.did.web.use.https=false |
identityhub.env, issuerservice.env |
NC-03 |
HTTPS DID documents with valid certificate |
| P7 |
NATS no TLS, no auth |
nats.conf |
NC-05 |
NATS TLS + NKey/Token authentication |
| P8 |
PostgreSQL no sslmode=require |
All JDBC URLs |
NC-03 |
sslmode=require + client certificate |
| P9 |
Traefik dashboard insecure (port 8090) |
docker-compose.jad.yml |
NC-01 |
Disable dashboard or protect behind VPN |
| P10 |
Neo4j HTTP API over plain HTTP |
api/admin/policies/route.ts |
NC-03 |
Neo4j over BOLT+TLS; remove HTTP API usage |
| P11 |
IssuerService super-secret-key |
issuerservice.env |
IS-07 |
Vault-managed signing key; rotate on schedule |
| P12 |
Provisioner has full Vault sudo |
bootstrap-vault.sh |
IS-05 |
Least-privilege provisioner policy; break-glass only |
| P13 |
Vault JWT clock skew 60 s |
bootstrap-vault.sh |
COS-01 |
Reduce to 10 s; add token binding |
| P14 |
Neo4j hardcoded password |
docker-compose.yml |
COS-01 |
Vault dynamic DB credentials |
| P15 |
Containers may run as root |
docker-compose.jad.yml |
INF-07 |
user: 1000:1000 in all compose services |
Protocol Penetration Testing (production pentest, not demo)
Full red-team exercises against DSP and DCP:
- Replay attack with captured
ContractRequest messages
- Token binding bypass across participant contexts
- StatusList2021 availability attack (fail-open vs fail-closed)
- Data plane SSRF via crafted
dataAddress.endpoint
Container & Supply Chain Security
trivy image scan for all EDC-V, Keycloak, Vault images
- SBOM generation per image (SPDX format)
- OWASP Dependency-Check for EDC-V Java dependencies
- Signed container images (cosign)
Implementation Order (Demo Phase)
D1 → Cypher injection fix (30 min — code change)
D4 → npm audit in CI (15 min — workflow change)
D5 → detect-secrets baseline (20 min — tooling)
D2 → CSP headers in next.config (30 min — config change)
D3 → Security E2E test suite (2 h — new test file)
D8 → Rate limiting middleware (1 h — middleware change)
D6 → Threat model document (2 h — documentation)
D7 → BSI C5 gap analysis (3 h — documentation)
Acceptance Criteria (Demo Phase)
References
/cc @ma3u
Context
This is a Minimum Viable Demo (MVD) of an EHDS-compliant health dataspace. The stack is intentionally run in dev-mode (no TLS, hardcoded secrets, in-memory Vault) so it can be deployed with a single
docker compose up. The security assessment has two distinct tracks:The demo track does not require replacing Vault dev-mode or adding TLS — those are documented as known dev-only shortcuts. It does require fixing injection risks, proving auth works correctly, and producing the BSI C5 gap analysis that shows what would change in production.
Why BSI C5?
BSI C5 is the German Federal Office for Information Security's cloud security catalogue. Under EHDS Article 50(6), secondary-use health dataspaces must demonstrate conformance with a recognised security framework. BSI C5 is the applicable standard for EU health infrastructure. The demo proves we understand what needs to change and why — the production track delivers the actual changes.
🟢 Demo / MVD Phase — Implement Now
These are real security fixes that can and should be done in the demo codebase today. They demonstrate security maturity without requiring infrastructure changes.
D1 — Fix Cypher Injection Risk (CRITICAL for any phase)
File:
ui/src/app/api/admin/audit/route.tsRisk: The WHERE clause is built by string-escaping user input (
replace(/'/g, "\\'")). This is insufficient — parameterised Cypher must be used.BSI C5: DEV-07 (secure coding)
Demo value: Shows we understand and fix OWASP A03 (Injection) at the code level.
D2 — Add Content Security Policy Headers
File:
ui/next.config.tsRisk: No CSP → XSS can exfiltrate session tokens from the browser.
BSI C5: DEV-07, NC-03
Demo value: Shows headers are correct even in dev mode; easy to tighten in production.
D3 — Security E2E Test Suite (
__tests__/e2e/security/)Playwright tests that prove the auth and middleware actually work:
GET /adminwithout session → redirects to/auth/signin(not 200)GET /compliancewithout session → redirects to/auth/signinGET /patient/profilewithout session → redirects to/auth/signinGET /api/admin/policieswithout session → 401 or redirectGET /api/graphwithout session → 200 (public, not protected)GET /catalogwithout session → 200 (public, not protected)X-Content-Type-Options: nosniffX-Frame-Options: DENYlocalStorageon/does not containNEXTAUTH_SECRETor any token/auth/signinpage does not expose Keycloak client secret in page sourceDemo value: These tests run in CI and prove that route protection is not accidentally disabled by a future refactor.
D4 —
npm auditin CI (Zero High/Critical CVEs)Add to
.github/workflows/test.yml:Demo value: Shows the dependency supply chain is monitored. Any new CVE fails the build.
D5 —
detect-secretsbaseline (no secrets in repo)Commit
.secrets.baselineso future commits are checked against it in pre-commit.Demo value: Proves no real credentials are accidentally committed —
.env.examplevalues are clearly placeholders.D6 — DSP / DCP Threat Model Document
File:
docs/security/threat-model.mdDocument the 7 protocol-level attack vectors for DSP and DCP with:
@ididempotency")Demo value: Shows evaluators that we have analysed the protocols — not just wired them up.
D7 — BSI C5 Gap Analysis Document
File:
docs/security/bsi-c5-gap-analysis.mdFor each of the 10 BSI C5 domains in scope:
Demo value: The gap analysis IS the deliverable for a demo — it shows we know what C5 requires and have a credible plan.
D8 — Rate Limiting on Admin API Routes
File:
ui/src/middleware.tsAdd a simple in-memory rate limiter for
/api/admin/*routes (e.g. max 60 req/min per IP).Demo value: Shows awareness of API abuse risk; trivial to replace with Redis-backed limiter in production.
🔵 Production Phase — Document Now, Implement Later
These require infrastructure changes and are out of scope for the demo. They are documented here so a production team has a clear checklist.
Infrastructure Hardening (not needed for demo)
identityhub.env,issuerservice.env-key-shares=1)vault-init-or-unseal.shvault-init-or-unseal.shtls_disable = truein Vaultvault.hclsslRequired: nonein Keycloakkeycloak-realm.jsonsslRequired: all; external TLS terminationedc.iam.did.web.use.https=falseidentityhub.env,issuerservice.envnats.confsslmode=requiresslmode=require+ client certificateport 8090)docker-compose.jad.ymlapi/admin/policies/route.tssuper-secret-keyissuerservice.envbootstrap-vault.shbootstrap-vault.shdocker-compose.ymldocker-compose.jad.ymluser: 1000:1000in all compose servicesProtocol Penetration Testing (production pentest, not demo)
Full red-team exercises against DSP and DCP:
ContractRequestmessagesdataAddress.endpointContainer & Supply Chain Security
trivyimage scan for all EDC-V, Keycloak, Vault imagesImplementation Order (Demo Phase)
Acceptance Criteria (Demo Phase)
api/admin/audit/route.tsuses parameterised Cypher — no string interpolation in WHERE clausesX-Frame-Options+X-Content-Type-Optionsheaders on all Next.js responses__tests__/e2e/security/contains ≥ 10 passing security tests (SEC-01–SEC-10)npm audit --audit-level=highpasses in CI for UI and neo4j-proxy.secrets.baselinecommitted; pre-commitdetect-secretshook activedocs/security/threat-model.mdcovers DSP + DCP protocol attack vectorsdocs/security/bsi-c5-gap-analysis.mdcovers all 10 BSI C5 domains with demo vs production status/api/admin/*routesReferences
/cc @ma3u