- 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>
94 lines
2.8 KiB
Go
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
|
|
}
|