Troubleshooting
Quick reference for when things break. Each section starts with what you see, then why it happens, then how to fix it.
OPA bundle not loading
Symptom: OPA starts but token requests fail with policy denials immediately, or OPA logs show repeated bundle fetch errors like bundle load failed or Bundle download failed.
Causes:
- Wrong or missing
X-Bundle-API-Key— OPA's config sets this via thePBAC_BUNDLE_API_KEYenvironment variable. If OPA was started without it, or with a different value than the AS expects, every bundle fetch returns401. - AS not running yet — OPA starts before the AS (correct order: MySQL → AS → OPA). If OPA starts first, it will fail to fetch the bundle until the AS is up.
- Wrong bundle URL —
opa/config.yamlpoints tohttp://host.docker.internal:8080/bundles/pbac/bundle.tar.gz. On Linux hosts,host.docker.internalmay not resolve; you may need172.17.0.1or the host's actual IP. - AS bundle endpoint returning an error — the AS logs a
Bundle build failederror if the DB is unreachable when OPA requests the bundle.
Diagnose:
Check OPA's console logs (they are set to debug level by default):
docker logs pbac-opa --tail 50
Test the bundle endpoint directly from the host. Replace dev-bundle-key with whatever PBAC_BUNDLE_API_KEY is set to:
curl -v -H "X-Bundle-API-Key: dev-bundle-key" \
http://localhost:8080/bundles/pbac/bundle.tar.gz \
-o /dev/null
A 200 with a non-empty body means the AS side is fine. A 401 means the key is wrong or missing. A 500 means the AS had an internal error (check the AS logs).
Check that OPA has loaded a bundle successfully:
curl http://localhost:8181/v1/data/oauth/token -s | jq .
If this returns {} or a missing-rule error instead of a policy result structure, OPA has no policies loaded yet.
Fix:
-
Ensure
PBAC_BUNDLE_API_KEYis set to the same value when starting both the AS and OPA:PBAC_BUNDLE_API_KEY=my-secret ./opa/run_opa.sh -
Start services in order: MySQL first, then the AS, then OPA.
-
On Linux, replace
host.docker.internalinopa/config.yamlwith the actual host IP or use--network hostfor the OPA container. -
Wait 5–10 seconds after startup; OPA polls the bundle every 5–10 seconds (
polling.min_delay_seconds/max_delay_secondsinopa/config.yaml).
Token request denied unexpectedly
Symptom: POST /token returns {"error":"access_denied"} or {"error":"unauthorized_client"} when you expect a token to be issued. The request looks correct.
Causes:
- OPA is unreachable — if the AS cannot connect to OPA at
OPA_URL, token issuance fails. - OPA has no policies loaded — if the bundle hasn't been fetched yet, OPA returns an empty result which the AS treats as a denial.
- Missing or incorrect policy data — the token policy depends on
data.oauthentries (resource definitions, subject entitlements). Ifpolicy_datarows are missing from the DB, OPA evaluates against an empty dataset and denies. grant_typenot in the client's software statement — the token policy checks that the requested grant type is authorised for the client.- Client is disabled or revoked — the AS checks
enabled = trueandrevoked_at IS NULLbefore calling OPA. - Denylist — the token policy may include explicit deny rules for certain subjects or clients.
Diagnose:
Enable OPA decision logs (already on by default — decision_logs.console: true in opa/config.yaml). Look for the decision immediately after the failed token request:
docker logs pbac-opa --tail 20
You will see JSON lines with "result" and "input" — check result.allow, result.reason, and result.decision.
Check OPA directly with the same input your AS would send:
curl -s http://localhost:8181/v1/data/oauth/token \
-H "Content-Type: application/json" \
-d '{"input": {"request": {"grant_type": "client_credentials", "scopes": ["read"]}, "client": {"client_id": "my-client"}}}' \
| jq .
Check that OPA is reachable from the AS:
curl -s http://localhost:8181/health | jq .
Check the AS application logs for PDP evaluation failed or OPA returned error.
Fix:
- If OPA is unreachable: verify
OPA_URLis set correctly and OPA is running. - If policies are missing: wait for the bundle to reload (up to 10 seconds), or check
GET /bundles/pbac/bundle.tar.gzreturns a valid tarball. - If policy data is missing: provision the required
policy_datarows via the Admin API (POST /admin/policy-data) or seed the DB. - If the client config is wrong: check the client's
software_statementfield in the Admin UI or viaGET /admin/clients/{id}.
Introspect returns active: false
Symptom: POST /introspect returns {"active": false} for a token you just issued.
Causes:
- Token is expired — the AS checks
expires_atbefore calling OPA. - JIT single-use token already consumed — if the PDP set
jit_single_use: truein the token'spdpOutput, the first successful introspect deletes the token. A second call returnsactive: false. - Wrong scope on the calling token — the caller must present a Bearer token with
introspectionoruma_protectionscope, or authenticate via Basic auth with a client that is entitled to those scopes. An incorrect or insufficient caller token causes a401/403, notactive: false, but misconfigured clients can appear as denials. - OPA re-evaluation denied — introspect re-evaluates the token policy at call time. If policy data has changed (e.g. a subject was removed), the re-evaluation may deny.
- Client disabled or revoked — the client that originally received the token has been disabled since issuance.
- Token simply does not exist — the token was never issued (typo, wrong environment, wrong AS instance).
Diagnose:
Check OPA decision logs immediately after the introspect call:
docker logs pbac-opa --tail 20
Look for the allow and reason fields in the decision. The AS logs a deny audit entry at WARN level when introspection fails.
Check whether the token exists and when it expires (Admin API):
curl -s -H "X-Admin-API-Key: dev-admin-key" \
"http://localhost:8080/admin/audit-logs?limit=5" | jq .
Check the audit log for the specific token's most recent /introspect entries to see whether jit_single_use revocation happened.
For JIT single-use, look for an audit entry with "action": "token_revoked" and "reason": "jit_single_use" immediately after the first successful introspect.
Fix:
- Expired token: re-request a token from
/token. - JIT single-use: by design — request a new token for each use.
- Wrong caller scope: ensure the RS client has
introspectionoruma_protectionin its granted scopes. Check viaGET /admin/clients/{id}and verify thesoftware_statement.granted_resourcesor the policy data grants the right scopes. - OPA re-evaluation denied: check policy data for the subject/resource — provision missing entries via
POST /admin/policy-data.
Admin API returns 401 or 403
Symptom: Calls to /admin/** or /access/** return 401 Unauthorized or 403 Forbidden.
Cause:
The X-Admin-API-Key header is missing, blank, or does not match PBAC_ADMIN_API_KEY. When PBAC_ADMIN_API_KEY is set to a non-blank value, all /admin/** and /access/** requests require this header. In local dev the variable defaults to blank (open), so this only triggers in environments where the key was explicitly configured.
Diagnose:
Check what key the AS started with by looking at startup logs:
# If the key is set, you will NOT see this warning at startup:
# "SECURITY WARNING: Admin API is unprotected"
# If you do see that warning, the key is blank and the endpoint should be open.
Try the request with the key:
curl -v -H "X-Admin-API-Key: your-key-here" \
http://localhost:8080/admin/clients
Fix:
- Pass
X-Admin-API-Key: <value>in every admin request. - Verify the key matches the
PBAC_ADMIN_API_KEYenvironment variable the AS was started with. - In local dev without the variable set, omit the header entirely — the endpoint is open.
MySQL connection errors
Symptom: The AS fails to start with an error like Communications link failure, Access denied for user 'pbac', or Unknown database 'pbac'. Or the AS starts but requests fail with DB errors.
Causes:
- Docker not running or MySQL container not started — MySQL runs as a Docker container defined in
docker-compose.yml. - Wrong credentials — default credentials are
pbac/pbac-dev, databasepbac. If overridden viaSPRING_DATASOURCE_USERNAME,SPRING_DATASOURCE_PASSWORD, orSPRING_DATASOURCE_URL, a mismatch causes auth failure. - SSL misconfiguration — the base
application.ymlJDBC URL does not setuseSSL=false. The dev profile does adduseSSL=false&allowPublicKeyRetrieval=true. In production, if your MySQL instance requires SSL, ensure the keystore is configured. - Flyway migration failure — if the schema is out of sync (e.g., you pulled new code with new migrations but the DB was not updated), the AS will refuse to start with
Validate failed: detected resolved migration not applied. - Port conflict — MySQL is mapped to
3306. If another process holds that port, Docker fails to bind.
Diagnose:
Check if MySQL is running:
docker-compose ps
docker-compose logs mysql --tail 30
Test connectivity from the host:
mysql -h 127.0.0.1 -P 3306 -u pbac -ppbac-dev pbac -e "SELECT 1;"
Check AS startup logs for the exact exception. Flyway errors look like:
FlywayException: Validate failed: Detected resolved migration not applied to database
Fix:
- Start MySQL:
docker-compose up -d - Wait ~10 seconds for MySQL to initialise on first run (the AS may need to be restarted if it started before MySQL was ready).
- For Flyway migration failures: run
mvn flyway:migrateor let the AS apply migrations automatically on next startup (Flyway will apply pending migrations in order). - For SSL issues in production: either configure the JDBC URL with the appropriate SSL parameters, or confirm
useSSL=falseis acceptable for your network topology. - If port 3306 is in use: stop the conflicting process or change the port mapping in
docker-compose.ymland updateSPRING_DATASOURCE_URL.
Startup warnings about insecure defaults
Symptom: The AS logs lines like:
SECURITY WARNING: Admin API is unprotected — set PBAC_ADMIN_API_KEY to require authentication on /admin/** and /access/**
SECURITY WARNING: Bundle endpoint is unprotected — set PBAC_BUNDLE_API_KEY to require authentication on /bundles/**
What it means:
These warnings are emitted by SecurityConfig.warnIfUnprotected() at startup. They indicate:
PBAC_ADMIN_API_KEYis blank (the default) —/admin/**and/access/**are completely open to anyone who can reach the AS. Fine for local dev; dangerous in any shared or production environment.PBAC_BUNDLE_API_KEYisdev-bundle-keyor blank — the bundle endpoint has no real secret protecting it. OPA can fetch policy data without authentication.
Fix for production:
Set both variables to strong, randomly generated values before deployment:
export PBAC_ADMIN_API_KEY=$(openssl rand -hex 32)
export PBAC_BUNDLE_API_KEY=$(openssl rand -hex 32)
Then configure OPA to use the bundle key by setting PBAC_BUNDLE_API_KEY when starting OPA:
PBAC_BUNDLE_API_KEY="$PBAC_BUNDLE_API_KEY" ./opa/run_opa.sh
OPA's config.yaml passes this as the X-Bundle-API-Key header:
services:
as:
headers:
X-Bundle-API-Key: ${PBAC_BUNDLE_API_KEY}
The warnings disappear once both keys are set to non-blank values. If you intentionally want an open admin endpoint (e.g., behind a network firewall), you can acknowledge the warning and move on — it does not block startup.
Registration denied
Symptom: POST /register returns {"error":"registration_denied"} or {"error":"invalid_client_metadata: ..."}.
Causes:
jwks_urinot using HTTPS — if the registration request includes ajwks_uri, it must use thehttps://scheme. The AS rejectshttp://values withinvalid_client_metadata: jwks_uri must use HTTPS scheme. This check runs in bothRegisterService(DCR) andClientAssertionValidator.- Policy denied the registration — the OPA register policy (
/v1/data/oauth/register) evaluated the DCR request and returneddecision: deny. This typically means the client's software statement does not satisfy the configured registration rules. - Untrusted software statement issuer — if
PBAC_SOFTWARE_STATEMENT_VERIFY=true, the AS verifies the JWT signature of the software statement. An unknown issuer or invalid signature will cause rejection. - Missing required claims — the register policy checks for required claims in the software statement (e.g.,
grant_types,redirect_urisfor public clients,software_id). Missing claims produce a denial with areasonfield in the OPA output. private_key_jwtwithout JWKS — if the client registers withtoken_endpoint_auth_method: private_key_jwtbut provides neitherjwks_urinor an inlinejwks, registration is rejected.- Step-up required — the policy may return
decision: step_upwith obligations (e.g., requiring additional verification before registration proceeds). The AS returns a403with anobligationsfield.
Diagnose:
The error response body includes a reason field from the OPA policy when available. Check it first:
curl -s -X POST http://localhost:8080/register \
-H "Content-Type: application/json" \
-d '{"client_name":"test","grant_types":["client_credentials"]}' \
| jq .
Check OPA decision logs for the register evaluation:
docker logs pbac-opa --tail 20
The input to the register policy is logged in the AS audit log — check GET /admin/audit-logs for recent /register entries to see the full pdpInput and policyDecision.
Test the register policy directly against OPA with your input:
curl -s http://localhost:8181/v1/data/oauth/register \
-H "Content-Type: application/json" \
-d '{"input": {"request": {"grant_types": ["client_credentials"], "client_name": "test"}, "software_statement": {}}}' \
| jq .result
Fix:
- Use
https://forjwks_uri. In local dev, use a tunnelling tool (e.g.,ngrok) to expose a local JWKS endpoint over HTTPS, or register with an inlinejwksinstead. - If the policy is denying: inspect the
reasonfield and adjust the software statement claims to match what the policy requires. Seeopa/policies/oauth/register.regofor the rules. - If signature verification is failing: either set
PBAC_SOFTWARE_STATEMENT_VERIFY=falsefor testing, or ensure the software statement is signed by an issuer with a reachable JWKS endpoint. - For step-up: check the
obligationsfield in the403response — it describes what additional steps are required before registration can proceed.
Next steps
- OPA Setup — Bundle server integration and OPA configuration details
- Configuration — Environment variables and production hardening checklist
- Observability — Structured logging and Grafana dashboards for monitoring