Remote Bundle Configuration
SAPL PDP nodes can fetch .saplbundle files from a remote HTTP server. This enables
centralized policy distribution without requiring filesystem access on the node.
Deployment Models
- Open core: Any HTTP server (S3, CDN, Nginx, Artifactory) serves bundles as static files.
- Enterprise: A Policy Administration Point (PAP) manages bundles for a node cluster using the same HTTP protocol.
Enabling Remote Bundles
Set the PDP configuration type to REMOTE_BUNDLES:
io.sapl.pdp.embedded:
pdpConfigType: REMOTE_BUNDLES
remoteBundles:
baseUrl: https://pap.example.com/bundles
pdpIds:
- production
- staging
Bundles are addressed by convention: {baseUrl}/{pdpId}. The example above resolves to:
GET https://pap.example.com/bundles/productionGET https://pap.example.com/bundles/staging
Configuration Reference
All properties live under io.sapl.pdp.embedded.remoteBundles:
| Property | Type | Default | Description |
|---|---|---|---|
baseUrl |
String |
(required) | Base URL of the bundle server. |
pdpIds |
List<String> |
(required) | PDP identifiers to fetch bundles for. |
mode |
POLLING or LONG_POLL |
POLLING |
Change detection mode. |
pollInterval |
Duration |
30s |
Interval between polls (POLLING mode). |
longPollTimeout |
Duration |
30s |
Server hold time (LONG_POLL mode). |
authHeaderName |
String |
(none) | HTTP header name for authentication. |
authHeaderValue |
String |
(none) | HTTP header value for authentication. |
followRedirects |
boolean |
true |
Follow HTTP 3xx redirects. |
pdpIdPollIntervals |
Map<String, Duration> |
(empty) | Per-pdpId poll interval overrides. |
firstBackoff |
Duration |
500ms |
Initial backoff after a fetch failure. |
maxBackoff |
Duration |
5s |
Maximum backoff after repeated failures. |
Change Detection
Regular Polling (works with any HTTP server)
The node sends GET {baseUrl}/{pdpId} at the configured interval. HTTP conditional
requests (If-None-Match with ETag) avoid redundant downloads. The server responds
304 Not Modified if the bundle has not changed.
io.sapl.pdp.embedded:
remoteBundles:
mode: POLLING
pollInterval: 30s
Long-Poll (requires server support)
The node sends GET {baseUrl}/{pdpId} with If-None-Match. The server holds the
connection until the bundle changes or a timeout occurs. On change, the server responds
200 OK with the new bundle. On timeout, the server responds 304 Not Modified and the
node reconnects immediately.
io.sapl.pdp.embedded:
remoteBundles:
mode: LONG_POLL
longPollTimeout: 30s
If the server does not support long-polling (responds immediately with 304), the behavior degrades gracefully to regular polling.
Authentication
The node sends a configurable HTTP header on every request:
io.sapl.pdp.embedded:
remoteBundles:
authHeaderName: Authorization
authHeaderValue: Bearer eyJhbGciOiJSUz...
This covers OAuth2 bearer tokens, static API keys, and custom authentication headers.
Both authHeaderName and authHeaderValue must be provided together or both omitted.
Bundle Security
Remote bundles use the same signature verification as local bundles via the shared
bundleSecurity configuration block. Signatures are mandatory by default for remote
bundles.
io.sapl.pdp.embedded:
pdpConfigType: REMOTE_BUNDLES
bundleSecurity:
publicKeyPath: /path/to/key.pub
# OR
publicKey: MCowBQYDK2VwAyEA...
# Per-tenant key bindings (optional)
keys:
prod-key: MCowBQYDK2VwAyEA...
tenants:
production: [prod-key]
For individual tenants that should accept unsigned bundles without enabling the global escape hatch, use the unsignedTenants list:
bundleSecurity:
publicKeyPath: /path/to/key.pub
unsignedTenants:
- development
- staging
Tenants listed here may load unsigned bundles while all other tenants still require valid signatures.
For development only, the 2-factor escape hatch disables signature verification globally:
bundleSecurity:
allowUnsigned: true
acceptRisks: true
Per-pdpId Poll Interval
Each pdpId inherits the global pollInterval unless overridden:
io.sapl.pdp.embedded:
remoteBundles:
pollInterval: 60s
pdpIdPollIntervals:
staging: 10s # Override for staging
In this example, production polls every 60 seconds while staging polls every 10
seconds.
Health and Lifecycle
The node exposes three health states via Spring Boot Actuator:
| State | Condition | Health Status |
|---|---|---|
| DOWN | No bundle fetched yet (startup) | DOWN |
| UP | Bundle loaded, remote reachable | UP |
| DEGRADED | Bundle loaded, remote unreachable | UP (with warning) |
At startup, the node is DOWN. It transitions to UP per-pdpId as each bundle is successfully fetched. If the remote becomes unreachable after a successful fetch, the node continues serving the last-known bundle in DEGRADED state.
Size Limit
Remote bundle responses are limited to 16 MB. Bundles exceeding this limit are rejected. This limit is enforced by the client and cannot be configured.
Retry Behavior
On fetch failure, the node retries with exponential backoff (with jitter). The backoff
starts at firstBackoff and caps at maxBackoff. After recovery, the backoff resets
to the initial value.
Graceful Shutdown
On application shutdown, all fetch loops are cancelled and HTTP connections are released. No manual intervention is needed.
Programmatic Configuration
For non-Spring environments, the builder API supports remote bundles directly:
var securityPolicy = BundleSecurityPolicy.builder(publicKey).build();
var config = new RemoteBundleSourceConfig(
"https://pap.example.com/bundles",
List.of("production"),
RemoteBundleSourceConfig.FetchMode.POLLING,
Duration.ofSeconds(30),
Duration.ofSeconds(30),
"Authorization", "Bearer token",
true, securityPolicy,
Map.of(),
Duration.ofMillis(500),
Duration.ofSeconds(5),
WebClient.builder());
var pdp = PolicyDecisionPointBuilder.withDefaults(mapper, clock)
.withRemoteBundleSource(config)
.build()
.pdp();