Identity-VPN Add-On
Optional paid add-on: key-custody adapters, deployment recipes, operator runbook, per-jurisdiction compliance posture.
Status: v1 (2026-05) — optional paid add-on Audience: product, sales, DevOps, hospital IT Companion docs:
- Architecture:
docs/architecture/vpn-audit-compliance.md - Service README:
services/identity-vpn/README.md - Frontend miniapp:
web/packages/miniapps/identity-vpn/README.md
1. What it is
Identity-VPN is a paid add-on to medOS that bundles:
- Per-staff WireGuard tunnels — clinicians get an audited, encrypted route into the hospital network from any device (phone, laptop, shared workstation).
- Compliance-grade audit log — every connect / disconnect / config-issue / revoke is signed (Ed25519), hash-chained, and retained per jurisdiction (TH, JP, PH, EU, UK).
- Hourly Merkle anchoring — the audit log is provably tamper-evident; off-site WORM export lets you prove integrity to a regulator months later.
- Encrypted reverse-lookup — raw IPs and device IDs are never stored on audit events; they live in separate envelope-encrypted side-tables that only the SOC can decrypt.
- Staff-facing enrollment UI — drop into the medOS web app at
/admin/identity-vpn, scan QR with the WireGuard mobile app, done.
It does not replace medOS’s normal authentication (JWT / passkey) — those still mediate every API call. The VPN is the network layer; auth is the application layer.
2. When to enable it — and when to skip
| Situation | Recommendation |
|---|---|
| Hospital already runs Pulse Secure / Cisco AnyConnect / Tailscale / corporate WireGuard | Skip. Their existing VPN gates network access; medOS just rides over it. |
| Greenfield hospital with no remote-access solution | Enable. Identity-VPN is cheaper and more medical-audit-aware than buying a third-party VPN. |
| Hospital wants a unified audit trail (who reached what, when) joined to EHR audit | Enable. The correlationId on every audit event ties VPN sessions to EHR record access. |
| Patient-facing telehealth (video, file upload) | Not yet — patient lane is on the roadmap; for now patients use the public TLS endpoints. |
| Air-gapped hospital with no remote staff | Skip. No remote access means no tunnel to issue. |
| Cross-hospital admin needs reach into multiple sites | Enable at each site that allows egress; couriered audit for any sites that don’t. |
Default state across the platform is OFF. Three independent gates (Compose profile, gateway env, frontend env) all default off; flipping any single one without the others does nothing.
3. What’s included
| Layer | Component | Path |
|---|---|---|
| Schema | VpnPeer, VpnAuditEvent, VpnAuditLedger, VpnAuditIpmap, VpnAuditDevicemap, VpnLedgerLock |
packages/platform-api-schema/src/identityVpn/ |
| Backend service | ever-api-identity-vpn (NestJS + Moleculer) |
services/identity-vpn/ |
| Gateway routes | /api/v2/identityvpn/* (whitelisted when IDENTITY_VPN_ENABLED=true) |
services/gateway/ |
| Frontend miniapp | “My VPN devices” page with QR enrollment, admin user-picker | web/packages/miniapps/identity-vpn/ |
| Compose service | api-identity-vpn behind the identity-vpn profile |
infrastructure/docker-compose-onpremise.yml |
| Setup script | One-shot interactive installer with key bundle generation | infrastructure/scripts/setup-onpremise.sh |
| Key-init script | Standalone CLI for key generation + rotation | services/identity-vpn/scripts/init-key.js |
4. Architecture in one paragraph
A clinician opens /admin/identity-vpn in the medOS web app, taps Add device, and chooses BYOD / hospital laptop / shared workstation. The ever-api-identity-vpn backend generates a fresh X25519 keypair server-side, allocates a tunnel-internal IPv4, writes a VpnPeer row in Mongo, and pushes the peer’s public key to the WireGuard server (via the wg-cli adapter on-prem, or noop when the operator provisions WireGuard by hand). The clinician scans the returned QR code with the WireGuard mobile app — the tunnel comes up. Every step writes a signed audit event; an hourly cron aggregates the last hour’s events into a Merkle root, signs it, and exports a JSON bundle to a directory the operator rsyncs off-host on their own schedule.
WireGuard apps (mobile/laptop)
│ tunnel
▼
┌──────────────────────┐
│ WireGuard server │ ← operator provisions (host network)
│ (wg0, 10.88.0.0/16) │
└──────────┬───────────┘
│ wg set peer ...
▼
┌──────────────────────────────────────────────┐
│ ever-api-identity-vpn │
│ - peer registry (VpnPeer) │
│ - signed audit log (VpnAuditEvent) │
│ - encrypted side-tables (Ipmap/Devicemap) │
│ - hourly Merkle anchor (VpnAuditLedger) │
│ - WORM bundle export (local-dir) │
└──────────┬─────────────────────┬─────────────┘
│ NATS │ files
▼ ▼
Moleculer mesh /opt/medos/worm/{TH,JP,PH,EU-*,UK}/YYYY/MM/DD/
→ rsync to NAS / burn to media
5. Deployment models
5a. On-prem (primary)
Single host or small Docker cluster. The hospital owns the database, the WireGuard server, the key material, and the WORM archive. medOS provides:
- The Compose stack
- The
setup-onpremise.sh --identity-vpninstaller - Ongoing software updates
cd infrastructure
./scripts/setup-onpremise.sh --region japan --identity-vpn
# Installer:
# - generates the file-encrypted key bundle into ./identity-vpn-keys/
# - writes the passphrase to a sibling file (you back it up)
# - configures docker-compose with the --profile identity-vpn flag
docker compose --env-file .env.onpremise.japan \
-f docker-compose-onpremise.yml \
--profile identity-vpn up -d
5b. Hybrid (medOS-managed control plane, hospital-managed data plane)
Backend services run on medOS-managed infrastructure (PH demo ALB, or a regional VPS). The WireGuard server runs at the hospital. Useful for:
- Customers who want the audit log managed by us
- Pilot deployments before committing to on-prem
5c. Pure cloud (future)
When AWS KMS + S3 Object Lock adapters land — for customers who already operate in AWS.
6. Key custody — choose the right model
The identity-vpn service signs every audit event with an Ed25519 key and envelope-encrypts side-table records under a KEK (key encryption key). Where those two keys live is the single most important deployment decision.
Adapter comparison
| Adapter | Where keys live | Ops cost | Latency | When to pick |
|---|---|---|---|---|
process-local |
RAM, ephemeral | Zero | Microseconds | Dev only — keys die on restart, audit events stop verifying |
file-encrypted ✅ shipped |
Passphrase-wrapped JSON on the application host | Operator manages passphrase + backups | Microseconds (in-process) | On-prem default. Sufficient for ~90% of hospitals. |
vault ✅ shipped |
HashiCorp Vault transit engine, on-prem | Run a Vault server (single container or HA cluster) | 5-50ms LAN round-trip | Hospital already uses Vault, or wants HA + centralised rotation without the cloud |
pkcs11 ⏳ reserved |
YubiHSM / Nitrokey / network HSM | Hardware purchase, HSM admin | 10-100ms | High-security / compliance-mandated HSM-rooted keys |
| TPM-sealed ⏳ reserved | Sealed under host TPM 2.0 chip | Tied to host hardware | Microseconds | “Theft of the laptop running this must not expose the key” |
aws-kms ⏳ reserved |
AWS KMS, US/EU regions | $1/mo per CMK + $0.03/10k calls + network | 50-200ms | Hybrid customer already in AWS / hospital data-residency in US / compliance signalling |
What “Vault adapter” actually means
HashiCorp Vault is an open-source secrets management server. Its transit secrets engine stores cryptographic keys inside Vault and exposes HTTP APIs that perform operations on those keys without returning the key material to the caller:
POST /v1/transit/sign/medos-vpn-signer→ returns an Ed25519 signaturePOST /v1/transit/verify/medos-vpn-signer→ confirms a signaturePOST /v1/transit/encrypt/medos-vpn-kek→ returns an envelope-encrypted DEKPOST /v1/transit/decrypt/medos-vpn-kek→ returns the unwrapped DEK
The Vault adapter for identity-vpn implements IKeyCustody as a ~250-line HTTP client over those endpoints. Authentication uses AppRole (role-id + secret-id, similar to OAuth client credentials) so the service can authenticate without a human-held token. Self-signed Vault is supported via VAULT_CA_CERT_FILE.
Setup (one operator command):
export VAULT_ADDR=https://vault.medos.local:8200
export VAULT_TOKEN=hvs.<operator-token>
./services/identity-vpn/scripts/init-vault.sh
# Mounts the transit engine, creates the two keys (Ed25519 signer + AES KEK),
# writes a least-privilege policy, creates an AppRole, prints role-id + secret-id.
Rotation:
vault write -f transit/keys/medos-vpn-signer/rotate
# Old audit events stay verifiable — every event's signerKeyId carries the
# version (vault:medos-vpn-signer:v3) and Vault keeps all prior versions.
Why pick Vault over file-encrypted?
- Key material never sits on the application host. Even root on the medOS server can’t extract the private key.
- Centralised rotation. Vault rotates the key with one API call; the identity-vpn service picks up the new version automatically.
- Vault has its own audit log of every key operation — useful for compliance reviews.
- HA out of the box. Two-node Vault cluster keeps the signing path available through a single failure.
- Backend can be an HSM. Vault transit can use an HSM as its storage backend, giving you HSM-grade isolation at the operational cost of Vault.
Why might you skip Vault?
- One more service to run, monitor, and unseal after restart
- ~5-50ms round-trip per audit event signature (vs in-process microseconds with
file-encrypted) - Vault license complexity if you want the Enterprise features (HA replication, namespaces) — but Vault OSS is free and sufficient for this workload
Recommended path: start with file-encrypted. Move to vault when a customer asks for HA-safe keys, centralised rotation, or HSM rooting. Skip AWS KMS unless the customer is fully in AWS.
Key-custody operator runbook
| Task | Command | Frequency |
|---|---|---|
| First-time key generation | node services/identity-vpn/scripts/init-key.js --out /opt/medos/keys/vpn-signing.json (auto-run by setup-onpremise.sh --identity-vpn) |
Once at install |
| Backup the key bundle | Copy /opt/medos/keys/vpn-signing.json + the passphrase to your secrets manager (1Password, Bitwarden, sealed envelope) |
Immediately after install, then before any rotation |
| Rotate the signing key | node init-key.js --out new.json --rotate-from old.json then swap the env var |
Annually, or after suspected compromise |
| Verify off-site archive | sha256sum -c /opt/medos/worm/.../anchor-*.json.sha256 |
Quarterly spot-check |
7. Operational responsibilities (who does what)
| Task | Hospital IT | medOS team |
|---|---|---|
| Provision the WireGuard server (kernel module, firewall, port forward) | ✓ | help available |
| Generate + back up the key bundle | ✓ | tool provided |
| Rsync WORM bundles to a NAS or media | ✓ | format documented |
| Quarterly audit-log review | ✓ | report queries provided |
| Service updates (medOS releases) | — | ✓ |
| Compliance answers for regulators | shared | shared |
8. Compliance posture (summary)
See docs/architecture/vpn-audit-compliance.md for the full per-jurisdiction analysis.
| Jurisdiction | Key duties this add-on covers | What the hospital still owns |
|---|---|---|
| Thailand (PDPA) | Section 37 security measures, ROPA, access-log retention | PDPC notifications, patient-disclosure workflow |
| Japan (APPI + MHLW v6.0) | Tamper-evident audit, per-record retention, “3省2ガイドライン” controls | Cross-border consent under APPI Art. 28 |
| Philippines (DPA + NPC C-2023-06) | Differential security/system log retention, immutability, quarterly review | NPC PIA documentation, NPC breach notification |
| EU (GDPR + NIS2 + EHDS + ISO 27799) | Art. 32 security, NIS2 logging, EHDS correlationId propagation, ISO 27789 fields |
DPO appointment, Member State filings, HDS hosting if French |
| UK (DSPT) | DSPT-aligned 6-year retention, ICO breach format | DSPT toolkit certification |
9. Pricing / commercial positioning
The add-on is priced separately from the medOS core platform. Tier positioning, license terms, and per-region pricing are owned by the commercial team — contact them for an SOW.
Operator effort to enable for a paying customer is < 30 minutes with the automated installer; the licence flip is one env var.
10. Roadmap / what’s deferred
| Item | Status | When |
|---|---|---|
vault key-custody adapter |
✅ Shipped | — |
aws-kms adapter |
Reserved | When first AWS customer commits |
pkcs11 / TPM adapters |
Reserved | High-security customer driven |
| S3 Object Lock WORM exporter | Reserved | Cloud rollout |
| Patient telehealth tunnel | Roadmap | After staff lane stabilises in production |
| ACL change events tied to roster | Roadmap | Depends on roster integration design |
| NIS2 CSIRT structured export | Roadmap | First EU deployment |
| Last-handshake live dashboard | Built (poller ships off-by-default) | Enable when WireGuard data plane goes live |
| Real-KMS hot signing on the hot path | Not recommended | Use file-encrypted or vault — KMS round-trip per event is the wrong fit |
11. Quick-start recipe
# 1. Setup (interactive)
cd infrastructure
./scripts/setup-onpremise.sh --region japan --identity-vpn
# 2. Provision the WireGuard server (one-time, host-side)
sudo apt install wireguard
sudo wg genkey | tee /etc/wireguard/server-private.key | wg pubkey > /etc/wireguard/server-public.key
sudo nano /etc/wireguard/wg0.conf # Address = 10.88.0.1/16, ListenPort = 51820, PrivateKey from file above
sudo wg-quick up wg0
echo "VPN_SERVER_PUBLIC_KEY=$(cat /etc/wireguard/server-public.key)" >> .env.onpremise.japan
echo "VPN_WG_DATAPLANE=wg-cli" >> .env.onpremise.japan
# 3. Start medOS with the VPN profile
docker compose --env-file .env.onpremise.japan \
-f docker-compose-onpremise.yml \
--profile identity-vpn up -d
# 4. Back up the key bundle off-host
cp -p identity-vpn-keys/vpn-signing.json identity-vpn-keys/passphrase /mnt/backup/secure/
# 5. First clinician enrolls a device
# Open https://medos.local/admin/identity-vpn → "Add device" → scan QR
12. Support escalation
| Issue | First action |
|---|---|
| Audit events not signing | Check service logs for “Using file-encrypted key custody” — if instead you see “Ephemeral keys generated”, the passphrase/key file isn’t being loaded |
| WORM bundles not appearing | Confirm VPN_WORM_EXPORT=local-dir and VPN_WORM_DIR is writable from the container |
| Multiple anchor rows for the same hour | Multi-replica without lock — verify the migration to vpn_ledger_locks was applied |
| Frontend route is 404 | VITE_FEATURE_IDENTITY_VPN not set on the Vercel/build env |
API 404 on /api/v2/identityvpn/* |
IDENTITY_VPN_ENABLED not true on the gateway env |
| Last-handshake column always empty | VPN_HANDSHAKE_POLLER not true, or VPN_WG_DATAPLANE=noop |