Security & multi-tenancy
A clinical data repository holds PHI, so its access controls and audit trail are part of the product, not an afterthought. This chapter covers the four security surfaces you configure when you deploy EHRbase-rs: authentication (who is calling), authorization (what they may do), multi-tenancy (isolating independent logical systems), and the ATNA audit trail (recording what happened). Each is independently configurable, and each is described here in terms of the environment variables you actually set.
Configuration follows the same pattern throughout: the server reads defaults,
then an optional TOML file, then environment variables, with __ separating
nested keys. The three security configuration groups use distinct prefixes —
EHRBASE_REST_ (authentication and tenancy), EHRBASE_AUTHZ_ (authorization),
and EHRBASE_ATNA_ (audit) — and each also accepts a TOML file path
(EHRBASE_REST_CONFIG, EHRBASE_AUTHZ_CONFIG, EHRBASE_ATNA_CONFIG).
Authentication
Authentication is on by default (EHRBASE_REST_AUTH__ENABLED=true). Setting
it to false lets all requests through unauthenticated — a development-only
mode.
There is no single “mode” switch. The server offers two mechanisms and enables each by the presence of its configuration block:
- HTTP Basic is active when a
basicblock with a user list is configured. Each user has a username, an Argon2 password hash (a PHC string beginning$argon2id$), and a set of roles (default["USER"]). Because it is a list of users, the Basic block is normally supplied through the TOML configuration file rather than environment variables. - OAuth2/OIDC bearer tokens are active when an
oidcblock is configured. The server validates the token’s signature, issuer, and (optionally) audience.
The OIDC settings:
| Environment variable | Default | Meaning |
|---|---|---|
EHRBASE_REST_AUTH__OIDC__ISSUER | — (required to enable OIDC) | expected iss, and the OIDC discovery base |
EHRBASE_REST_AUTH__OIDC__AUDIENCES | empty (not checked) | accepted aud values |
EHRBASE_REST_AUTH__OIDC__ALGORITHMS | ["RS256"] | accepted signing algorithms |
EHRBASE_REST_AUTH__OIDC__HMAC_SECRET | unset | an HS256 symmetric secret (development/testing) |
EHRBASE_REST_AUTH__OIDC__JWKS_JSON | unset | a static JWKS document |
There is no separate JWKS or discovery URL to set: the server discovers the
JWKS URI from the issuer’s .well-known/openid-configuration unless you supply
a static JWKS_JSON (preferred when present) or an HMAC_SECRET.
Tip
Keycloak example. Point the issuer at your realm and let discovery do the rest:
export EHRBASE_REST_AUTH__OIDC__ISSUER=https://keycloak.example/realms/ehrbase export EHRBASE_REST_AUTH__OIDC__AUDIENCES=ehrbase-apiThe same pattern works for Active Directory or any standards-compliant identity provider. Prefer JWKS/discovery over a shared HS256 secret in production.
An unauthenticated request to a protected route is refused with 401; an
authenticated request that lacks the required role is refused with 403.
Authorization
Authorization has two composable layers. The coarse layer is always on when authentication is enabled; the fine-grained layer is opt-in.
RBAC (role-based, coarse)
Every operation is classified as Public, Clinical, Management, or Admin, and a
role model gates each class. Roles are plain, case-insensitive strings; the
defaults are USER (the baseline clinical role) and ADMIN.
| Environment variable | Default | Meaning |
|---|---|---|
EHRBASE_AUTHZ_RBAC__ENABLED | true | the coarse role gate (active only when auth is enabled) |
EHRBASE_AUTHZ_RBAC__ADMIN_ROLE | ADMIN | role required for admin operations |
EHRBASE_AUTHZ_RBAC__USER_ROLE | USER | the baseline clinical role |
EHRBASE_AUTHZ_RBAC__ROLE_CLAIMS | ["realm_access.roles","scope"] | JWT claim paths mined for roles |
EHRBASE_AUTHZ_RBAC__MANAGEMENT_ACCESS | admin_only | management-surface access: admin_only, private, or public |
Roles come from the JWT claims listed in ROLE_CLAIMS — by default the
Keycloak realm_access.roles array plus the space-separated scope claim —
or from a Basic user’s configured roles. A clinical operation needs at least one
role; an admin operation needs the admin role; the management surface follows
its tri-state setting. Disabling RBAC restores authentication-only behaviour.
ABAC (attribute-based, fine-grained)
For attribute-level decisions — “may this user touch this patient’s data, under this organisation, for this template?” — enable ABAC. A policy decision point is consulted per clinical operation with resolved attributes.
| Environment variable | Default | Meaning |
|---|---|---|
EHRBASE_AUTHZ_ABAC__ENABLED | false | master ABAC switch |
EHRBASE_AUTHZ_ABAC__ENGINE | cedar | cedar (embedded) or remote (external PDP) |
EHRBASE_AUTHZ_ABAC__ORGANIZATION_CLAIM | organization_id | JWT claim for the organisation attribute |
EHRBASE_AUTHZ_ABAC__PATIENT_CLAIM | patient_id | JWT claim for the patient attribute (enables the subject gate) |
EHRBASE_AUTHZ_ABAC__CEDAR__POLICY_DIR | — (required for cedar) | directory of .cedar policy files |
EHRBASE_AUTHZ_ABAC__CEDAR__RELOAD_SECS | off | optional policy hot-reload interval |
EHRBASE_AUTHZ_ABAC__REMOTE__SERVER | — (required for remote) | PDP base URL (must end with /) |
EHRBASE_AUTHZ_ABAC__REMOTE__CONNECT_TIMEOUT_MS | 2000 | PDP connect timeout |
EHRBASE_AUTHZ_ABAC__REMOTE__REQUEST_TIMEOUT_MS | 5000 | PDP request timeout |
Two engines sit behind one interface. Cedar is the embedded default:
policies live in .cedar files, are schema-validated at boot (an invalid
policy set stops the server rather than silently denying), and need no
external service. The remote PDP option consults an external policy server
over HTTP for deployments that already run one.
Warning
Authorization is fail-closed: if the policy engine is unreachable or a policy cannot be evaluated, the request is refused (mapped to
500), never permitted. When a patient claim is configured, a local subject gate also rejects access to another patient’s EHR before any policy call. A denied decision is a403.
Multi-tenancy
Multi-tenancy lets one deployment host several isolated logical openEHR
systems, each with its own system_id. It is off by default; when off, the
server behaves byte-for-byte as a single-tenant system.
| Environment variable | Default | Meaning |
|---|---|---|
EHRBASE_REST_TENANCY__ENABLED | false | enable multi-tenancy |
EHRBASE_REST_TENANCY__CLAIM | tenant | the JWT claim (a dotted path) carrying the tenant key |
EHRBASE_REST_TENANCY__HEADER | unset | a development header override for the tenant |
A request’s tenant is resolved from the configured JWT claim (a dotted path
such as realm_access.tenant is walked through nested objects). Isolation is
enforced in the database with PostgreSQL row-level security: the resolved
tenant scopes the connection so a query can only ever see its own tenant’s
rows.
Warning
Leave
EHRBASE_REST_TENANCY__HEADERunset in production — a client-supplied header must never be able to select a tenant; the tenant must come from the authenticated token. Isolation is also fail-safe by design: an absent or unresolvable tenant runs unscoped against a reserved default rather than guessing, and a cross-tenant access surfaces as an empty result set, never a403that would leak the existence of another tenant’s data.
ATNA audit trail
Separately from openEHR’s own provenance, EHRbase-rs can emit an IHE ATNA
security audit trail: one DICOM Audit Message (DICOM PS3.15 §A.5) per audited
operation, describing who did what to which resource, with what
outcome, from where, and when. Records are shipped to an Audit Record
Repository over syslog (RFC 5424 framing), transported over UDP (RFC 5426) or
TLS (RFC 5425). Every server operation is audited, and authentication failures
(401/403) are always recorded.
| Environment variable | Default | Meaning |
|---|---|---|
EHRBASE_ATNA_ENABLED | false | master switch |
EHRBASE_ATNA_REPOSITORY_HOST | localhost | audit repository host |
EHRBASE_ATNA_REPOSITORY_PORT | 514 | audit repository port |
EHRBASE_ATNA_TRANSPORT | udp | udp or tls |
EHRBASE_ATNA_ENTERPRISE_SITE_ID | unset | enterprise/site identifier |
EHRBASE_ATNA_SOURCE_ID | ehrbase | audit source identifier |
EHRBASE_ATNA_VALUE_IF_MISSING | UNKNOWN | fill for an empty mandatory field |
EHRBASE_ATNA_SUPPRESS_LOGIN_EVENTS | true | skip the successful-login records |
EHRBASE_ATNA_FAIL_MODE | open | open (drop and continue) or closed (reject auditable ops if undeliverable) |
EHRBASE_ATNA_RESOLVE_SUBJECT | false | enrich the patient identifier from stored data |
EHRBASE_ATNA_QUEUE_CAPACITY | 1024 | bounded in-memory audit queue |
EHRBASE_ATNA_SERVER_HOST | unset | this node’s advertised network address |
EHRBASE_ATNA_TLS_CA_PATH | unset | PEM of the repository CA (TLS) |
EHRBASE_ATNA_TLS_IDENTITY_CERT_PATH | unset | client certificate PEM (mutual TLS) |
EHRBASE_ATNA_TLS_IDENTITY_KEY_PATH | unset | client key PEM (mutual TLS) |
For PHI-adjacent audit, use EHRBASE_ATNA_TRANSPORT=tls with a CA (and, where
the repository requires it, a client certificate and key for mutual TLS). The
fail_mode choice is a policy decision: open never blocks a clinical
request when the repository is down (records are dropped and metered), while
closed refuses auditable operations with 503 rather than proceed
unaudited.
Note
The ATNA trail is orthogonal to openEHR’s own
CONTRIBUTIONandAUDIT_DETAILS, which the server always writes in the same transaction as every change. openEHR audit records what a version says about its own authorship; ATNA records security surveillance of API access. Both coexist. Identified data never enters telemetry (metrics, traces, logs) — see Operations — so the audit trail is the single place where access to identified data is recorded.