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>
121 lines
5.7 KiB
Markdown
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. |
|