guildhouse-spire-plugins/cmd/oidc-attestor/plugin.go
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

94 lines
2.8 KiB
Go

package main
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/guildhouse-cooperative/guildhouse-spire-plugins/pkg/oidc"
)
// OIDCAttestor implements the SPIRE WorkloadAttestor plugin interface.
//
// When SPIRE Agent needs to attest a workload, it calls Attest() with the
// workload's process ID. This plugin reads the workload's OIDC token and
// returns selectors based on the verified claims.
//
// Selectors produced:
// - oidc:sub:<subject> — OIDC subject claim
// - oidc:iss:<issuer> — OIDC issuer
// - oidc:email:<email> — OIDC email claim (if present)
// - oidc:group:<group> — One per OIDC group claim (if present)
type OIDCAttestor struct {
verifier oidc.Verifier
audience string
tokenPath string // Path pattern for discovering OIDC tokens (supports /proc/<pid>/root/ prefix)
}
// OIDCAttestorConfig holds plugin-specific configuration.
type OIDCAttestorConfig struct {
Issuer string `hcl:"issuer"`
Audience string `hcl:"audience"`
JWKSURL string `hcl:"jwks_url"`
TokenPath string `hcl:"token_path"` // e.g., "/var/run/secrets/tokens/oidc-token"
}
// Configure initializes the attestor with the provided configuration.
func (a *OIDCAttestor) Configure(cfg OIDCAttestorConfig) error {
if cfg.TokenPath == "" {
cfg.TokenPath = "/var/run/secrets/tokens/oidc-token"
}
verifier, err := oidc.NewVerifier(oidc.Config{
Issuer: cfg.Issuer,
Audience: cfg.Audience,
JWKSURL: cfg.JWKSURL,
})
if err != nil {
return fmt.Errorf("oidc-attestor: configure verifier: %w", err)
}
a.verifier = verifier
a.audience = cfg.Audience
a.tokenPath = cfg.TokenPath
return nil
}
// Attest reads the OIDC token for the given PID and returns selectors.
func (a *OIDCAttestor) Attest(ctx context.Context, pid int32) ([]string, error) {
// Read the token from the workload's filesystem namespace.
tokenFile := filepath.Join(fmt.Sprintf("/proc/%d/root", pid), a.tokenPath)
tokenBytes, err := os.ReadFile(tokenFile)
if err != nil {
return nil, fmt.Errorf("oidc-attestor: read token for pid %d: %w", pid, err)
}
rawToken := strings.TrimSpace(string(tokenBytes))
if rawToken == "" {
return nil, fmt.Errorf("oidc-attestor: empty token for pid %d", pid)
}
claims, err := a.verifier.Verify(ctx, rawToken, a.audience)
if err != nil {
return nil, fmt.Errorf("oidc-attestor: verify token for pid %d: %w", pid, err)
}
// Build selectors from verified claims.
var selectors []string
if claims.Subject != "" {
selectors = append(selectors, "oidc:sub:"+claims.Subject)
}
if claims.Issuer != "" {
selectors = append(selectors, "oidc:iss:"+claims.Issuer)
}
if claims.Email != "" {
selectors = append(selectors, "oidc:email:"+claims.Email)
}
for _, group := range claims.Groups {
selectors = append(selectors, "oidc:group:"+group)
}
return selectors, nil
}