Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 basic block 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 oidc block is configured. The server validates the token’s signature, issuer, and (optionally) audience.

The OIDC settings:

Environment variableDefaultMeaning
EHRBASE_REST_AUTH__OIDC__ISSUER— (required to enable OIDC)expected iss, and the OIDC discovery base
EHRBASE_REST_AUTH__OIDC__AUDIENCESempty (not checked)accepted aud values
EHRBASE_REST_AUTH__OIDC__ALGORITHMS["RS256"]accepted signing algorithms
EHRBASE_REST_AUTH__OIDC__HMAC_SECRETunsetan HS256 symmetric secret (development/testing)
EHRBASE_REST_AUTH__OIDC__JWKS_JSONunseta 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-api

The 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 variableDefaultMeaning
EHRBASE_AUTHZ_RBAC__ENABLEDtruethe coarse role gate (active only when auth is enabled)
EHRBASE_AUTHZ_RBAC__ADMIN_ROLEADMINrole required for admin operations
EHRBASE_AUTHZ_RBAC__USER_ROLEUSERthe baseline clinical role
EHRBASE_AUTHZ_RBAC__ROLE_CLAIMS["realm_access.roles","scope"]JWT claim paths mined for roles
EHRBASE_AUTHZ_RBAC__MANAGEMENT_ACCESSadmin_onlymanagement-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 variableDefaultMeaning
EHRBASE_AUTHZ_ABAC__ENABLEDfalsemaster ABAC switch
EHRBASE_AUTHZ_ABAC__ENGINEcedarcedar (embedded) or remote (external PDP)
EHRBASE_AUTHZ_ABAC__ORGANIZATION_CLAIMorganization_idJWT claim for the organisation attribute
EHRBASE_AUTHZ_ABAC__PATIENT_CLAIMpatient_idJWT 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_SECSoffoptional policy hot-reload interval
EHRBASE_AUTHZ_ABAC__REMOTE__SERVER— (required for remote)PDP base URL (must end with /)
EHRBASE_AUTHZ_ABAC__REMOTE__CONNECT_TIMEOUT_MS2000PDP connect timeout
EHRBASE_AUTHZ_ABAC__REMOTE__REQUEST_TIMEOUT_MS5000PDP 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 a 403.

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 variableDefaultMeaning
EHRBASE_REST_TENANCY__ENABLEDfalseenable multi-tenancy
EHRBASE_REST_TENANCY__CLAIMtenantthe JWT claim (a dotted path) carrying the tenant key
EHRBASE_REST_TENANCY__HEADERunseta 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__HEADER unset 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 a 403 that 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 variableDefaultMeaning
EHRBASE_ATNA_ENABLEDfalsemaster switch
EHRBASE_ATNA_REPOSITORY_HOSTlocalhostaudit repository host
EHRBASE_ATNA_REPOSITORY_PORT514audit repository port
EHRBASE_ATNA_TRANSPORTudpudp or tls
EHRBASE_ATNA_ENTERPRISE_SITE_IDunsetenterprise/site identifier
EHRBASE_ATNA_SOURCE_IDehrbaseaudit source identifier
EHRBASE_ATNA_VALUE_IF_MISSINGUNKNOWNfill for an empty mandatory field
EHRBASE_ATNA_SUPPRESS_LOGIN_EVENTStrueskip the successful-login records
EHRBASE_ATNA_FAIL_MODEopenopen (drop and continue) or closed (reject auditable ops if undeliverable)
EHRBASE_ATNA_RESOLVE_SUBJECTfalseenrich the patient identifier from stored data
EHRBASE_ATNA_QUEUE_CAPACITY1024bounded in-memory audit queue
EHRBASE_ATNA_SERVER_HOSTunsetthis node’s advertised network address
EHRBASE_ATNA_TLS_CA_PATHunsetPEM of the repository CA (TLS)
EHRBASE_ATNA_TLS_IDENTITY_CERT_PATHunsetclient certificate PEM (mutual TLS)
EHRBASE_ATNA_TLS_IDENTITY_KEY_PATHunsetclient 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 CONTRIBUTION and AUDIT_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.