Skip to main content

Harden an instance

Use this page before a self-hosted instance serves real traffic. On a hosted environment, PolicyArc sets these values for you.

Set the required secrets​

The authorization server refuses to start when any of these five values is blank. Generate each one per instance with openssl rand -hex 32, and never copy one between instances.

VariableHelm valueWhat it protects
PBAC_ADMIN_API_KEYsecret.adminApiKeyThe Admin API and admin UI under /admin/**. The AuthZEN routes under /access/** do not use it. They take a Bearer token or a client's Basic credentials.
PBAC_ADMIN_PEPPERsecret.adminPepperThe hashes used to look up admin keys
PBAC_CREDENTIAL_KEYsecret.credentialKeySecrets and keys stored in the database. See Keys and secrets.
PBAC_BUNDLE_API_KEYsecret.bundleApiKeyThe policy bundle that the policy engine fetches from /bundles/**
PBAC_GATEWAY_LINK_STATE_SECRETsecret.linkStateSecretThe signed links that send a user to connect an upstream account

A blank value stops the start with a message that names it, such as pbac.admin.api-key (PBAC_ADMIN_API_KEY) is required. We started the server image with each value left blank in turn, and each start exited with such a message.

A blank PBAC_BUNDLE_API_KEY falls back to the value dev-bundle-key, and the server refuses that value too. So does a known development value for the pepper or the credential key.

Leave the development override off​

PBAC_ALLOW_INSECURE_DEV_KEY=true, or authServer.allowInsecureDevKey: true in the chart, lets the server start without those values. It then uses public values from the source tree for the credential key, the pepper, the bundle key and the link secret. With a blank admin key, it serves /admin/** to anyone. /access/** still asks for a Bearer token or Basic credentials. The dev Spring profile turns the override on. Leave the override unset and do not run the dev profile.

Set these values​

VariableDefaultSet it to
PBAC_OIDC_ISSUERhttp://localhost:8080The public URL of the instance. Tokens and discovery name this issuer. The chart requires authServer.issuer.
PBAC_SWAGGER_ENABLEDtruefalse. When true, /v3/api-docs and /swagger-ui.html answer without a key and describe the Admin API operations. The chart does not set it, so add it through authServer.extraEnv. We rendered the chart with it there, and the variable reached the server container.
PBAC_SESSION_COOKIE_SECUREfalsetrue when TLS ends at a proxy, so the browser session cookie is only sent over HTTPS. The chart sets true. We have not checked the flag on a live session cookie.
PBAC_TRUSTED_PROXY_COUNT0The number of proxies in front of the server. The server then reads the client address from X-Forwarded-For. At 0 it uses the address of the connection, so behind a proxy every audit row and every rate limit bucket sees the proxy's address. The chart sets 1.

Keep these at their defaults.

VariableDefaultWhat the default does
PBAC_SOFTWARE_STATEMENT_VERIFYtrueVerifies the signature of a software statement at /register
PBAC_SAFE_HTTP_ALLOW_INSECURE_LOCALHOSTfalseRefuses plain http:// and private addresses when the server fetches a client's key set, a software statement issuer's discovery document, or an identity provider's discovery document from the Admin API's discovery call
PBAC_RATE_LIMIT_ENABLEDtrueLimits each client address to PBAC_RATE_LIMIT_REQUESTS requests, 1000 by default, per PBAC_RATE_LIMIT_WINDOW seconds, 60 by default, on /token, /authorize, /register, /introspect, /revoke, /bc-authorize, /gateway/mcp, /gateway/link and /gateway/connect. Each replica counts on its own.

PBAC_ADMIN_ENABLED=false turns the Admin API and the admin UI off. A call with a valid admin key then gets HTTP 404. Use it only on an instance you never manage through the Admin API.

Block the metrics endpoint at your proxy​

/actuator/prometheus answers without a key, and so does /actuator/health. The chart's ingress sends every path to the server by default, so the metrics are public unless your proxy blocks the path. Block /actuator/prometheus at the ingress and scrape it from inside the cluster. The chart's liveness and readiness probes call /actuator/health/liveness and /actuator/health/readiness on the pod's port, not through the ingress. We read the ingress and the probes from a rendered chart and have not run them on a cluster.

Limit who holds an admin key​

Any admin key can read the plaintext of every other admin key. Mint one key per caller and revoke keys you no longer use. See Keys and secrets.

Check the instance from outside​

Run these checks from a machine outside your network, against the public URL. Set three values first.

export POLICYARC_URL="https://your-environment.example"
export POLICYARC_ADMIN_KEY="paste the Admin API key"
export POLICYARC_BUNDLE_KEY="paste the bundle key"

The block checks that the Admin API refuses a call with no key, that the bundle refuses a call with no key and with the public development key and accepts your bundle key, that the API description is off, and that discovery names the public URL.

status() { curl -sS -o /dev/null -w '%{http_code}' "$@"; }
check() { [ "$2" = "$3" ] && echo "ok $1" || { echo "FAIL $1 (HTTP $2, expected $3)" >&2; return 1; }; }
fails=0
check 'Admin API refuses a call with no key' "$(status "$POLICYARC_URL/admin/api/admin-keys")" 401 || fails=1
check 'Admin API accepts your key' "$(status "$POLICYARC_URL/admin/api/admin-keys" -H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY")" 200 || fails=1
check 'Bundle refuses a call with no key' "$(status "$POLICYARC_URL/bundles/pbac/bundle.tar.gz")" 401 || fails=1
check 'Bundle refuses the public development key' "$(status "$POLICYARC_URL/bundles/pbac/bundle.tar.gz" -H 'X-Bundle-API-Key: dev-bundle-key')" 401 || fails=1
check 'Bundle accepts your bundle key' "$(status "$POLICYARC_URL/bundles/pbac/bundle.tar.gz" -H "X-Bundle-API-Key: $POLICYARC_BUNDLE_KEY")" 200 || fails=1
check 'API description is off' "$(status "$POLICYARC_URL/v3/api-docs")" 404 || fails=1
issuer=$(curl -fsS "$POLICYARC_URL/.well-known/openid-configuration" | jq -r .issuer)
check 'Discovery names the public URL' "$issuer" "$POLICYARC_URL" || fails=1
[ "$fails" = 0 ]

Each line prints ok or FAIL. We ran the block against a server image started with the values on this page, and every line printed ok. We also ran it against a development instance, which failed the bundle and API description lines.

Then check the metrics path. Expect the status code your proxy returns for a blocked path, such as 403 or 404, and not 200.

curl -sS -o /dev/null -w '%{http_code}\n' "$POLICYARC_URL/actuator/prometheus"

We have not run this check behind a proxy. Without one, the server answers 200.