guildhouse-spire-plugins/docs/oidc-attestation.md
Tyler King 420a4e2ea0 Remediate all 17 audit findings from AUDIT.md
Critical fixes:
- F-01: SatScope array form support (single pointer → slice with polymorphic JSON)
- F-02: Add governance-intent@guildhouse.dev as 10th Shellstream extension
- F-06: Replace os.Exit(1) stubs with go-plugin Serve() boilerplate in all cmd/
- F-13: Validate SatScope.ResourcePattern is non-empty

High priority:
- F-03: Add normative Accord policy syntax note to credential-governance.md §8.2
- F-04: Replace OID XXXXX placeholder with explicit PEN reference and IANA TODO
- F-05: Document CredentialComposer hook mapping in spec and plugin-types.md
- F-07/F-08: Commit CI pipeline (.github/workflows/ci.yaml)
- F-09: Add hashicorp/go-plugin v1.6.3 to go.mod

Medium priority:
- F-10: Wire sample-ssh-cert-extensions.json fixture into shellstream tests
- F-11: Cross-reference merkle proof depth limit (256 leaves) in governance spec
- F-12: Add YAML format clarification headers to deploy configs
- F-14: Expand README with project status, docs links, and quick-start

Low priority:
- F-15: Standardize "SSH SVID" → "SSH-SVID" terminology across docs
- F-16: Add GovernanceEpochSeconds to PluginConfig and deploy configs
- F-17: Add troubleshooting section to deployment.md, error handling to OIDC docs

Global: Rename all extension keys from @guildhouse.io to @guildhouse.dev

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:45:33 -05:00

121 lines
5.7 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.io/ns/prod/sa/web-server \
-parentID spiffe://guildhouse.io/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.io/ns/prod/sa/admin-tool \
-parentID spiffe://guildhouse.io/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. |