Skip to content

Security & Compliance

Security is enforced at several layers: transport, request handling, identity, and โ€” after a request completes โ€” an audit record of what happened. None of these depend on which process, decision, or form a given request touches; they apply to every request the same way.


Transport

Traffic to the platform's own services is encrypted end to end. TLS certificates are provisioned and renewed automatically for the components behind the reverse proxy, and the managed hosting environment for the API itself handles its own certificate lifecycle.


Request-level protections

Every response carries a Content Security Policy restricting where scripts, styles, and images may be loaded from, and HTTP Strict Transport Security instructing browsers to only ever reach the platform over HTTPS. Both are set by Helmet, which HELMET_ENABLED can switch off and which is on by default.

Requests are rate-limited. One general limit applies to every request, authenticated or not, keyed per client IP; the ValidSign callback is exempt and has its own limiter. The limiter runs before any route authenticates the caller, so the per-tenant keying that RATE_LIMIT_PER_TENANT asks for never has a tenant to key on: in practice every bucket is per client IP. A separate, stricter limit โ€” 10 requests per 15 minutes per IP โ€” applies to the three public endpoints that accept a write without a login: submitting a use case, uploading a file for one, and sending feedback. The use-case and feedback submissions also require a solved ALTCHA proof-of-work challenge, which stands in for a login wall on a surface that deliberately has none; the check is skipped when no ALTCHA_HMAC_KEY is configured, and the file upload is not challenged.

Cross-origin access is governed by CORS, which is a browser control, not an access boundary. For a request from an origin that is not configured, the backend withholds the CORS response headers, so the browser does not hand the response to the calling page โ€” but the request itself is still handled. Requests without an Origin header (server-to-server calls, curl, health probes) are allowed through. /v1/openapi.json is open to every origin, as the NL API Design Rules require for a published description.


Authentication and authorization

Every protected endpoint requires a valid, signature-verified token before any request data is processed โ€” see Authentication & IAM for the full validation chain, the role checks a caller's token is subject to, and the tenant boundary that keeps one caller from reaching another tenant's resources. The cross-tenant machine-to-machine routes under /v1/m2m additionally require the token to have been issued to a client on M2M_ALLOWED_CLIENTS; any other token, a person's included, is refused with 403 M2M_CLIENT_NOT_ALLOWED.

The eDOCS routes under /v1/edocs are closed to citizens and to unknown machine clients: a person needs the caseworker or admin role, and a machine client must be on EDOCS_ALLOWED_CLIENTS. A person works in eDOCS as themselves, with the Entra ID token Keycloak stored when they signed in with their Flevoland account. The backend reads that token from Keycloak's broker endpoint with the person's own Keycloak token, so it can only ever read the tokens of the person making the request; it keeps them in memory only, per person and bounded, and never logs, returns or stores them. The bearer token the backend keeps on the request for that purpose is non-enumerable, so a log line or a copy of the request's authentication data never carries it. See eDOCS โ€” Live Testing.

Accepted risk: a browser-held token can reach eDOCS as its owner

The broker endpoint requires Keycloak's read-token role in the caller's access token, so every user holds it and ronl-business-api's access tokens carry it โ€” including the token the browser holds. A stolen Keycloak access token can therefore read its owner's stored Entra token from Keycloak's broker endpoint and reach eDOCS as that person outside the Business API's audit, for the Entra ID token's remaining life (about an hour). The stored refresh token is of no use without the client secret. The risk is accepted pending a decision on a backend-only path (issue 325).


Secrets management

Credentials โ€” database connection strings, the Operaton, eDOCS and ValidSign credentials, the Anthropic API key, and the like โ€” are held as environment configuration on the hosting platform, not committed to the repository. Only template files documenting which variables are expected are version-controlled. The Business API's own Keycloak client, ronl-business-api, is a public client: it has no client secret to hold. In production the boot refuses a missing Anthropic API key and one still set to a placeholder value.


Audit logging

Every authenticated request is recorded once it completes, capturing who made it (the caller's identity and tenant), what it was (the HTTP method and endpoint, and the resource type and id it addressed where the path identifies one), when it happened, the caller's IP address where that is configured to be captured, and the outcome (success, failure, or error, derived from the response status). Route handlers add their own entries for specific actions โ€” a process start, a task completion, a refused start โ€” with details of their own. Two paths are excluded: reading the audit log itself, and the AI assistant's chat turns. Audit logging can be disabled entirely by configuration, and IP capture can be disabled independently of the rest of the record.

Each entry is written to the audit database and also to the application log at info level.

The backend never deletes audit records. AUDIT_LOG_RETENTION_DAYS (default 2555, seven years) is parsed into configuration and read by no code: nothing purges audit records, and nothing applies a retention period to them.


Data handling

The log level is set by LOG_LEVEL (default info) in every environment; nothing ties it to the deployment tier. Logs and audit records are not free of personal data:

  • The BRP person lookup's own log lines carry no citizen service number (BSN): it logs the user, the tenant, the query type and the number of subjects asked for, and on a failure the upstream status, problem code or error message โ€” never the request body or the BRP response (#241).
  • The same lookup's audit entry stores the BSN it looked up, on purpose. Every audit entry is also written to the application log at info level, so that BSN reaches the application log through the audit record.

The frontend does not write a BSN to the browser console.

An unhandled error is answered with a generic message in production. Handled errors can carry upstream detail: a failed process start returns Operaton's own error message and the engine URL, and a BRP error returns the BRP response.


Electronic signatures

A user task can be signed electronically rather than merely approved โ€” a RIP phase-exit approval on the Infra-board, or a besluit in the caseworker task inbox. The signing platform is ValidSign, the EU-branded OneSpan Sign.

Three properties matter for this page:

  • Signing is opt-in from the process model, activated by an attribute on a single user task. A task without it behaves exactly as before, which is every ordinary task. Every task view checks for it before showing anything, so a task that must be signed is never offered its ordinary approval form instead.
  • Live signing is gated by an allowlist that is empty by default. The licence is production-only with an account-wide key and no sandbox tenant, so a misconfigured environment cannot fire a real signature request: stub mode must be off, a key present, and the deployment tier explicitly named.
  • The signed document and its evidence summary are archived in eDOCS, named after the signed template and the case's business key, and the process task completes only once the signature has landed โ€” through the platform's callback or a periodic sweep, whichever arrives first, on one idempotent path. An archive failure is recorded on the process but does not hold the task back: the signature is already legally complete at ValidSign.

The stub ceremony URL is a capability: package identifiers are random UUIDs, and the two unauthenticated routes it needs are rate-limited per client IP and answer 404 in live mode. Full detail, including which callback credential ValidSign actually sends, is on ValidSign signing.


Build and pipeline integrity

The delivery pipeline is itself a security surface, and is treated as one: every GitHub Actions reference is pinned to an immutable commit digest, the pipeline token is read-only unless a job demonstrably needs more, no git credential is left in the workspace after checkout, and a blocking audit gate enforces all three on every pull request. The backend reaches production through the same pipeline: a promotion to main runs the production deploy workflow, which authenticates to Azure over OIDC. Dependency updates are held for fourteen days before adoption, except security advisories, which bypass the wait.

For what is enforced, what cannot be, and where the coverage stops, see Supply-chain gate.


  • ValidSign signing โ€” the signing feature, its environment locks and its unauthenticated routes
  • Supply-chain gate โ€” pipeline pinning, least privilege, and the audit gate
  • Authentication & IAM โ€” the identity and tenancy checks this page's audit trail traces back to
  • API Design โ€” the response conventions error handling follows
  • Tasks and Processes โ€” the actions an audit record most often describes