- Network-policy SPIRE plugin extension - Governance event notification with merkle anchoring - Shellstream specs for consent channels + HFL embedded ABI - All 17 audit findings from AUDIT.md remediated - SSH credential composer + substrate key manager updates - Test coverage for config + sshcert packages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
127 lines
6.4 KiB
Markdown
127 lines
6.4 KiB
Markdown
# OIDC Workload Attestation
|
|
|
|
How the `oidc-attestor` plugin identifies workloads via OIDC tokens.
|
|
|
|
## Flow
|
|
|
|
```
|
|
Workload SPIRE Agent oidc-attestor OIDC Provider
|
|
| | | |
|
|
| FetchX509SVID | | |
|
|
|----------------->| | |
|
|
| | Attest(pid=1234) | |
|
|
| |------------------->| |
|
|
| | | GET /.well-known/ |
|
|
| | | openid-configuration|
|
|
| | |-------------------->|
|
|
| | | JWKS endpoint |
|
|
| | |<--------------------|
|
|
| | | |
|
|
| | | Read token from |
|
|
| | | /var/run/secrets/ |
|
|
| | | oidc/token |
|
|
| | | |
|
|
| | | Verify signature |
|
|
| | | via JWKS |
|
|
| | | |
|
|
| | Selectors | |
|
|
| |<-------------------| |
|
|
| X509-SVID | | |
|
|
|<-----------------| | |
|
|
```
|
|
|
|
## Token Discovery
|
|
|
|
The plugin discovers the workload's OIDC token via a configurable path. In Kubernetes, the token is typically projected into the pod filesystem:
|
|
|
|
```yaml
|
|
# Pod spec
|
|
volumes:
|
|
- name: oidc-token
|
|
projected:
|
|
sources:
|
|
- serviceAccountToken:
|
|
audience: spire
|
|
expirationSeconds: 3600
|
|
path: token
|
|
containers:
|
|
- name: app
|
|
volumeMounts:
|
|
- name: oidc-token
|
|
mountPath: /var/run/secrets/oidc
|
|
readOnly: true
|
|
```
|
|
|
|
Plugin configuration:
|
|
|
|
```hcl
|
|
WorkloadAttestor "guildhouse_oidc" {
|
|
plugin_cmd = "/opt/spire/plugins/oidc-attestor"
|
|
plugin_data {
|
|
issuer = "https://keycloak.guildhouse.example.org/realms/platform"
|
|
audience = "spire"
|
|
token_path = "/var/run/secrets/oidc/token"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Token Verification
|
|
|
|
1. Plugin reads the JWT from the configured path.
|
|
2. Fetches the OIDC discovery document from `{issuer}/.well-known/openid-configuration`.
|
|
3. Retrieves the JWKS from the `jwks_uri` in the discovery document.
|
|
4. Verifies the JWT signature using the matching key from the JWKS.
|
|
5. Validates standard claims: `iss` matches configured issuer, `aud` includes configured audience, `exp` is in the future.
|
|
|
|
## Selector Output
|
|
|
|
The plugin returns selectors that SPIRE uses to match workloads against registration entries:
|
|
|
|
| Selector | Source Claim | Example |
|
|
|----------|-------------|---------|
|
|
| `oidc_attestor:iss:<value>` | `iss` | `oidc_attestor:iss:https://keycloak.example.org/realms/platform` |
|
|
| `oidc_attestor:sub:<value>` | `sub` | `oidc_attestor:sub:f47ac10b-58cc-4372-a567-0e02b2c3d479` |
|
|
| `oidc_attestor:email:<value>` | `email` | `oidc_attestor:email:operator@guildhouse.coop` |
|
|
| `oidc_attestor:group:<value>` | `groups[]` | `oidc_attestor:group:platform-engineers` |
|
|
|
|
## Registration Entry Matching
|
|
|
|
To grant an SVID to workloads authenticated via OIDC:
|
|
|
|
```bash
|
|
spire-server entry create \
|
|
-spiffeID spiffe://guildhouse.dev/ns/prod/sa/web-server \
|
|
-parentID spiffe://guildhouse.dev/spire/agent/k8s_psat/guildhouse/... \
|
|
-selector oidc_attestor:sub:f47ac10b-58cc-4372-a567-0e02b2c3d479
|
|
```
|
|
|
|
Multiple selectors can be combined (AND logic):
|
|
|
|
```bash
|
|
spire-server entry create \
|
|
-spiffeID spiffe://guildhouse.dev/ns/prod/sa/admin-tool \
|
|
-parentID spiffe://guildhouse.dev/spire/agent/k8s_psat/guildhouse/... \
|
|
-selector oidc_attestor:iss:https://keycloak.example.org/realms/platform \
|
|
-selector oidc_attestor:group:platform-engineers
|
|
```
|
|
|
|
## JWKS Caching
|
|
|
|
The plugin caches JWKS responses for the duration specified by the `Cache-Control` header (or 5 minutes if not present). This avoids hitting the OIDC provider on every attestation.
|
|
|
|
## Error Handling
|
|
|
|
| Error Condition | Plugin Behavior | Impact |
|
|
|----------------|----------------|--------|
|
|
| **OIDC provider unreachable** | Returns an error from `Attest()`. SPIRE Agent logs the failure but may succeed with other attestors (e.g., `k8s`). | Workloads relying solely on OIDC attestation will not receive SVIDs until connectivity is restored. |
|
|
| **Token file not found** | Returns an empty selector set (no error). The workload may still match via other attestors. | No OIDC-derived selectors; registration entries requiring `oidc_attestor:*` selectors will not match. |
|
|
| **Token expired** | Returns an error. The `exp` claim is validated against the current time with a small clock skew tolerance. | Workload must refresh its projected token. Kubernetes projected tokens auto-rotate, so this typically resolves within the `expirationSeconds` window. |
|
|
| **JWKS key not found** | Returns an error. The token's `kid` header does not match any key in the cached JWKS. | May indicate key rotation at the OIDC provider. The plugin will refetch JWKS on the next cache expiry. |
|
|
| **Invalid token signature** | Returns an error. The token was not signed by a key in the JWKS. | Possible token tampering or misconfigured issuer. Check that `issuer` in plugin config matches the token's `iss` claim. |
|
|
| **Audience mismatch** | Returns an error. The token's `aud` claim does not include the configured `audience`. | Check that the projected ServiceAccountToken uses the correct `audience` value. |
|
|
|
|
## Security Note: Custom Claims
|
|
|
|
OIDC tokens may contain custom claims (e.g., `tenant_id`, `roles`) that are populated by the identity provider's protocol mappers. **These claims MUST NOT be trusted for authorization without server-side verification.** In self-managed Keycloak deployments, users can modify their own protocol mappers to inject arbitrary values into custom claims.
|
|
|
|
The `tenant_id` claim in particular MUST be cross-referenced against the server-side tenant registry. The plugin SHOULD derive tenant identity from the SPIRE registration entry or a trusted server-side mapping, not solely from the OIDC token's `tenant_id` claim.
|