Skip to main content

Data handling

This page lists what a self-hosted instance stores, where, how it is protected and when the server deletes it. It also lists the outside services the server calls. It describes the server and the policyarc-server Helm chart. It does not describe the hosted service.

Where data lives​

PlaceWhat it holds
The PostgreSQL databaseEvery record the server keeps. You run this database, see Deployment model. There is one exception. When PBAC_OIDC_JWKS_PATH is set, the server reads its signing key from that file, or generates one and writes the private key to it unencrypted. The setting is empty by default and the chart does not set it.
Each server pod's memoryCaches it rebuilds after a restart, such as the policy bundle, keys fetched from identity providers, the decrypted signing keys, Google service account credentials built from a connector secret, rate limit counts and open MCP streams
The OPA podA copy of the policy bundle, which holds the policy rules and the control data rows. OPA keeps the last bundle it activated in /tmp/opa, an emptyDir volume.
Standard output of each containerThe server's log lines and OPA's decision log. Your log collector decides where they go and how long they stay.

The chart has no other store. It runs no cache server, no object store and no mail server.

What the database holds​

Each row below names the tables, how the value is stored and what deletes it. "Encrypted" means encrypted with the credential key, described in the next section. We listed the tables of a test instance's database on version 0.65.0. The table covers each of them except flyway_schema_history, which records the migrations.

DataTablesHow it is storedDeleted by
People who signed insubject, subject_userinfoPlain. subject_userinfo holds the name, email, preferred username and whether the email is verified.No scheduled job. DELETE /admin/api/subjects/{id} removes the subject row and clears resource.owner_subject_id where it names the subject. Its subject_userinfo row, its tokens and its audit rows stay. Deleting the identity provider deletes its subject_userinfo rows.
Browser sessionsspring_session, spring_session_attributesPlain. spring_session_attributes.attribute_bytes holds the signed-in user's subject ID and claims.A cleanup every 5 minutes, set by PBAC_SESSION_CLEANUP_CRON. A session times out after PBAC_SESSION_TIMEOUT, 30 minutes by default.
Access tokensaccess_tokenThe token as issued, not hashed. Each row also holds the policy input and output, which include the client, the grant, the caller's IP address and user agent, and the user's claims for a user's token.A cleanup once the token has expired, every pbac.cleanup.interval-seconds, 3600 by default. Also revocation, the use of a single-use token, deleting the client, and a start with PBAC_CREDENTIAL_KEY_DISCARD_PER_USER=true.
Refresh tokensrefresh_tokenThe token as issued, with the policy input and the user's claimsThe same cleanup once expired, rotation when it is used, revocation, deleting the client or the identity provider, and a start with PBAC_CREDENTIAL_KEY_DISCARD_PER_USER=true
Sign-in in progressauthorization_request, authorization_code, idp_flowPlain. authorization_code.code is the code as issued and authorization_code.claims holds the user's claims. idp_flow holds the identity provider's ID token and the claims read from it.The cleanup deletes expired requests and codes, and a code is deleted when it is redeemed. An idp_flow row goes with its request. A request expires after 10 minutes and a code after 5, by default. Deleting the client deletes its requests and codes.
Approval requestsbackchannel_auth_requestPlain, including login_hint, binding_message, the policy input in pdp_input, and the approver's ID tokenThe cleanup once expired, and deleting the client or the approval identity provider
Replay defenceused_jti, dpop_noncePlainThe cleanup once expired
ClientsclientThe client secret as a BCrypt hash, and the previous secret's hash during a rotation. software_statement holds the claims of the statement the client registered with, and issued_software_statement_jwt the statement the server issued to it.DELETE /admin/api/clients/{clientId}, which also deletes the client's tokens, codes, authorization and approval requests, UMA permission tickets, UMA resources and UMA delegations
Identity providersidentity_providerThe client secret is encryptedDELETE /admin/api/identity-providers/{idpId}, which also deletes the provider's subject_userinfo, refresh_token, authorization_code, backchannel_auth_request and upstream_identity_token rows. It keeps access tokens and subjects, and clears their link to the provider.
Admin keysadmin_api_keyThe key is encrypted, plus a SHA-256 lookup hash mixed with PBAC_ADMIN_PEPPERNo scheduled job. A revoked key keeps its row.
Signing keysoidc_signing_keyThe private key is encryptedA sweep every pbac.oidc.key-sweep-seconds, 600 by default, deletes a retired key once its publication window has ended
Connector and resource credentialsconnector_secret, managed_resourceconnector_secret.value_enc and managed_resource.rs_client_secret_enc are encrypted. managed_resource.upstream_auth is plain, and a static header in it can hold a literal credential.No scheduled job
Each user's upstream credentialsupstream_identity_token, upstream_exchanged_token, user_upstream_secretEncrypted. The first holds a user's identity provider tokens, the second tokens exchanged from them, and the third a credential the user typed in.No scheduled job. A start with PBAC_CREDENTIAL_KEY_DISCARD_PER_USER=true deletes all three, and deleting the identity provider deletes its upstream_identity_token rows.
Policypolicy_rule, policy_dataPlainNo scheduled job
Decision auditaudit_logPlain. A row holds a summary of the request in request_summary, the policy input, the decision, the client, the subject, the IP address and the user agent.Never. See Audit retention.
Bundle auditbundle_audit_logPlain. A row holds the bundle's ETag and the previous one, the rule paths, the data keys, the bundle size, what triggered the build and who triggered it.Never
MCP sessionsmcp_sessionPlainAn hourly sweep, once expired
UMA permission ticketsuma_permission_ticketPlain, the ticket as issuedNothing deletes a ticket except deleting its client
UMA resources and delegationsuma_resource, uma_delegation, uma_delegation_scopePlain. uma_resource.owner_subject_id and the grantor and grantee subject IDs in uma_delegation identify people.Deleting the client
Configurationresource_server, resource_type, resource, scope, connector, connector_artifact, and the join tables resource_scope, resource_server_type and resource_type_scopePlainNo scheduled job

To keep a credential out of managed_resource.upstream_auth, store it as a connector secret and put a reference such as {{secret.jira_token}} in the static header value. The server replaces the reference with the decrypted secret on each call.

A token is stored as issued, so anyone who can read the access_token or refresh_token table can use a live token. Limit who can read the database the way you limit who can call the Admin API. We issued a token on a test instance and found it in access_token.token unchanged.

We deleted a subject that had signed in, through the Admin API on a test instance. The call returned HTTP 204. Its subject_userinfo row, with its name and email, its access token and its two audit rows were still there.

How secrets are encrypted​

PBAC_CREDENTIAL_KEY encrypts each column the table above marks as encrypted. The server uses AES-GCM through Spring Security's Encryptors.delux, which derives the AES key from the credential key. The server writes v1: before each value it encrypts. The prefix is the same for every key, so it does not tell you which key wrote a value, and the server also reads values written without it. No API returns a decrypted connector secret. GET /admin/api/admin-keys/{id}/value returns a decrypted admin key, and each call writes an audit row. Rotate keys and secrets says what happens when the key changes. Changing it by hand stops the server from starting.

Client secrets are not encrypted. They are BCrypt hashes, and the server shows a secret only once.

You can check a stored secret on your own instance. Set the server URL, an admin key, and a connection string for the database the server uses. The block needs psql, curl, od and base64.

export POLICYARC_URL="https://your-environment.example"
export POLICYARC_ADMIN_KEY="paste the Admin API key"
export POLICYARC_DATABASE_URL="postgresql://pbac:password@your-database.example:5432/pbac"

This block stores a secret through the Admin API, reads the stored column with psql, deletes the secret, and then checks the stored value. It deletes the secret before any check, so a failed check leaves nothing behind.

name="at_rest_check_$(date +%s)"
plain="visible-if-not-encrypted"
curl -fsS -o /dev/null -X POST "$POLICYARC_URL/admin/api/secrets" \
-H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" -H 'Content-Type: application/json' \
-d "{\"name\": \"$name\", \"value\": \"$plain\"}"
stored=$(psql "$POLICYARC_DATABASE_URL" -Atc "select value_enc from connector_secret where name = '$name'") || stored=""
curl -fsS -o /dev/null -X DELETE "$POLICYARC_URL/admin/api/secrets/$name" -H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY"
echo "stored value is ${#stored} characters and starts with ${stored:0:12}"
case "$stored" in v1:*) ;; *) echo "Expected a value that starts with v1:, got ${stored:0:12}" >&2; false ;; esac
# v1:, then hex of a 16-byte IV, the ciphertext and a 16-byte GCM tag
min=$((3 + 2 * (16 + ${#plain} + 16)))
[ "${#stored}" -ge "$min" ] || { echo "Expected at least $min characters, got ${#stored}" >&2; false; }
hex=$(printf %s "$plain" | od -An -tx1 | tr -d ' \n')
! grep -qF "$plain" <<<"$stored" || { echo 'The stored value holds the plaintext' >&2; false; }
! grep -qiF "$hex" <<<"$stored" || { echo 'The stored value holds the plaintext in hex' >&2; false; }
! grep -qF "$(printf %s "$plain" | base64)" <<<"$stored" || { echo 'The stored value holds the plaintext in base64' >&2; false; }

On our test instance the stored value was 115 characters and started with v1:. Creating and deleting the secret each write an audit row.

The block checks one of the nine encrypted columns, connector_secret.value_enc. It shows that the stored value is long enough to be the ciphertext and does not hold the plaintext as text, hex or base64. It does not show which key encrypted the value, or that the other eight columns are encrypted.

Audit retention​

The server never deletes audit rows and has no retention setting. Migration V73 adds triggers that reject UPDATE, DELETE and TRUNCATE on audit_log and bundle_audit_log. A database owner can disable them, and the server's own database role owns those tables. Audit log shows how to purge old rows.

What leaves the instance​

The server and OPA call these outside services. The chart's network policy lets the server reach any address on TCP 80 and 443, and OPA any address on TCP 443.

DestinationWhenWhat is sent
Identity providersA user signs in, the server reads the user's profile from the userinfo or introspection endpoint, a token is refreshed or exchanged for the managed gateway, an administrator runs discovery, or the server fetches the provider's discovery document and keysOAuth and OpenID Connect requests with the provider's client ID and secret, and the user's token for userinfo and introspection
Client key sets and client ID metadata documentsA client registers or authenticates with a key set URL or a metadata document URLA fetch of the URL
Software statement issuers and the SPIFFE trust bundle URLA statement or a JWT-SVID is checkedA fetch of the issuer's keys or the bundle
Upstream APIs of managed resourcesA client calls through the managed gateway and policy allows itThe call, with the upstream credential the resource is set up to use
Upstream MCP serversAn administrator asks the server to discover a managed resource's toolsAn MCP tool listing request
Google's token endpointThe gateway calls an upstream that uses a Google service accountA signed request for an access token
Google APIs, https://www.googleapis.com unless a policy sets api_baseA gateway obligation reads a file's labels or applies a labelCalls with the upstream credential of the request
The approval identity providerA CIBA request asks a person to approveA signed notification
AtlassianAn administrator looks up a site's cloud ID with GET /admin/api/connectors/atlassian-cloudidA fetch of https://<site>.atlassian.net/_edge/tenant_info
Your OTLP collectorOnly when you turn on telemetry exportTraces, metrics and logs. Telemetry export lists the fields.
LokiOnly when the server runs with the loki Spring profile, which the chart does not setThe server's log lines
OPA's version checkOPA starts. The chart runs OPA without --disable-telemetry.A request to the OPA project's telemetry service. Our local OPA logged OPA is out of date with the latest version, which is the reply. The chart has no value that turns this off.

The server checks four kinds of fetch before it makes them. They are a client's key set, a client ID metadata document, a software statement issuer's discovery document, and an identity provider's discovery document when an administrator runs discovery from the Admin API. For these it requires HTTPS, refuses private, loopback and link-local addresses, and does not follow redirects. PBAC_SAFE_HTTP_ALLOW_INSECURE_LOCALHOST relaxes this for listed development hosts and is off by default. The server does not check the identity provider's discovery document and key set that it fetches during sign-in, or the SPIFFE trust bundle. It fetches those from the URLs an administrator configured. Managed resource upstreams get the address check, but may use http. The trust center gives the detail.

Inside the instance, the server sends OPA the full input of each decision, and OPA fetches the policy bundle from the server.

Logs​

The chart sets no Spring profile, so the server writes JSON log lines to standard output. Monitor an instance shows a line. We read the server's INFO and WARN log statements and found none that writes a token, a secret or a claim value. Some WARN lines name a subject ID. A failed call to the token introspection endpoint from the gateway logs the response body, and an error from an outside call can include the text of the upstream response.

OPA writes a decision log line for each decision to its own standard output, because the chart's OPA configuration sets decision_logs to console. That line holds the full decision input, including the client, the IP address and the user's claims. Treat the OPA log as you treat the audit table.