guildhouse-spire-plugins/docs/oidc-attestation.md
Tyler King a58d548518 feat: network-policy extension, governance lifecycle, audit remediation
- 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>
2026-03-18 15:54:46 -04:00

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.