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>
5.7 KiB
5.7 KiB
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:
# 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:
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
- Plugin reads the JWT from the configured path.
- Fetches the OIDC discovery document from
{issuer}/.well-known/openid-configuration. - Retrieves the JWKS from the
jwks_uriin the discovery document. - Verifies the JWT signature using the matching key from the JWKS.
- Validates standard claims:
issmatches configured issuer,audincludes configured audience,expis 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:
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):
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. |