Billing Service — Security Model
Status: populated Owner: TBD Last updated: 2026-04-17 Companion: Service Template · 13 Security · 14 Extended compliance
1. Trust boundaries
2. AuthN
- Inbound: Bearer JWT issued by identity-service; validated via JWKS (
IDENTITY_JWKS_URL);iss,aud=billing-service,tid,subrequired. - Outbound: mTLS for service-to-service; service account JWT for terminology / fhir-gateway / tenant-service.
- Patient portal traffic enters through
patient-portal-serviceBFF; billing sees a downstream service JWT with patient scope claims.
3. AuthZ — RBAC / ABAC matrix
| Scope | Roles (default) | Purpose | ABAC |
|---|---|---|---|
billing:charge:read | billing_clerk, cashier, supervisor, auditor | Read charges | tid match |
billing:charge:write | billing_clerk, system (event handler) | Capture charges | tid + facility match |
billing:charge:reverse | supervisor | Reverse a posted charge | threshold amount per tenant config |
billing:account:read | billing_clerk, cashier, supervisor, patient (self via portal) | Read accounts + aging | Patient sees only patient_id == self |
billing:invoice:read | billing_clerk, cashier, supervisor, patient (self) | Read invoices | Patient scope filter |
billing:invoice:issue | billing_clerk | Issue a draft invoice | facility match |
billing:invoice:void | supervisor | Void an invoice | reason code required |
billing:payment:post | cashier, system (remittance) | Post payment | Idempotency enforced |
billing:refund:request | cashier | Request refund | any |
billing:refund:approve | supervisor, finance_manager | Approve refund | dual-approval if amount > tenant threshold |
billing:adjustment:post | biller, system (remittance) | Apply adjustment | reason code + facility match |
billing:statement:run | billing_admin | Start batch statement | facility match |
billing:pricelist:manage | tenant_admin | Manage price lists | facility match |
billing:gl:export | finance_manager | GL export | tenant match |
4. Licensing gate
Before any route handler executes, LicensingGuard calls tenant-service.checkEntitlement('billing'). If disabled → 403 MODULE_NOT_ACTIVE. Result cached per tenant for 60 s.
5. Data classification
| Field class | Examples | Storage |
|---|---|---|
| PHI | patient_id linkage, encounter context | Encrypted at rest (pgcrypto disk + KMS); RLS by tenant_id |
| Financial | Amounts, ledger entries | Encrypted at rest; integrity-protected by append-only trigger |
| Payer | Coverage refs passed in (no member IDs stored here) | References only, not raw PII |
| Secrets | Payment gateway creds, JWT keys | KMS or HashiCorp Vault; never in env files |
| PCI | Card data | Never stored — adapter tokens only |
6. Encryption
- At rest: Postgres with LUKS/EBS volume encryption; column-level AES-GCM via pgcrypto for
external_ref,reference, PII audit fields. - In transit: TLS 1.3 on all links; mTLS inside service mesh.
- KMS: AWS KMS / Vault Transit for data keys; annual rotation.
7. Audit events (emitted to audit-service)
| Event | When |
|---|---|
audit.billing.charge.captured | Charge posted |
audit.billing.charge.reversed | Charge reversed |
audit.billing.invoice.issued | Invoice issued |
audit.billing.invoice.voided | Invoice voided |
audit.billing.payment.posted | Payment posted |
audit.billing.refund.requested/approved/rejected/posted | Refund lifecycle |
audit.billing.adjustment.applied | Adjustment posted |
audit.billing.pricelist.published | Price list change |
audit.billing.gl.export | Export run |
audit.billing.access.unauthorized | 403 / cross-tenant attempt |
All audit events include actor_id, tenant_id, correlation_id, request_id, ip_hash, and a minimal resource_ref (no payloads with PII).
8. GDPR participation
| Right | Billing behaviour |
|---|---|
| Access | Patient can retrieve their invoices + payments via portal; export as FHIR Bundle |
| Rectification | Charge reversal + new charge; ledger immutability preserved |
| Erasure | Restricted — financial records retained per legal hold (commonly 10 y). Personal identifiers pseudonymised; monetary records retained |
| Portability | FHIR Bundle (Account + Invoices + Payments) export |
| Objection / restriction | Flag on account; block new charges |
9. Tenant isolation tests (mandatory)
tenant-isolation.spec.tsmust attempt cross-tenant read/write on every table; every attempt must return empty / 403 / RLS deny.- PostgreSQL
app.tenant_idsession variable must be set by every request handler before any query.
10. Key security controls
| Control | Implementation |
|---|---|
| Input validation | Zod schemas on every DTO, rejects unknown fields |
| SQL injection | Drizzle parameterised queries only; no string concat |
| XSS in PDFs | Server-side rendering with escaping; user-supplied text sanitised |
| CSRF | State-changing endpoints require Origin header match and Bearer auth (no cookies) |
| Rate limiting | Kong: 100 rps per client per route; POST /payments 20 rps per tenant |
| Bot protection | Kong bot detector on patient-facing routes |
| Data loss prevention | No PAN/CVV storage; static analysis blocks logs of sensitive fields |
| Supply chain | npm audit --production; trivy image scan in CI |
| Secret rotation | 90 d for service JWT signing key; 30 d for gateway adapter tokens |