guildhouse-spire-plugins/GSAP-ATTESTOR-DESIGN.md
Tyler J King fe5e2cf3c6 feat(spire): gsap-attestor WorkloadAttestor plugin
SPIRE WorkloadAttestor that reads governance env vars from /proc/{pid}/environ
(walking up the process tree to find gsh) and emits gsap: selectors on workload
SVIDs. Maps BASCULE_* vars set by bascule-shell and future GSH_* vars to the
11-selector vocabulary defined in gsap-types/src/selectors.rs.

- pkg/gsap/selectors.go: shared Go constants mirroring Rust vocabulary
- cmd/gsap-attestor/: plugin implementation with /proc reading, process tree
  walking, capability ceiling translation, and fail-open for non-governed processes
- 28 tests covering selector extraction, proc parsing, tree walking, and depth limits
- Makefile, Dockerfile, deploy config updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-13 03:59:08 -04:00

6.9 KiB
Raw Blame History

GSAP Attestor — Design Document

SPIRE WorkloadAttestor plugin that reads governance context from the process environment and emits GSAP selectors on workload SVIDs.

Selector Vocabulary

The Go constants in pkg/gsap/selectors.go mirror the Rust definitions in substrate-gsap/crates/gsap-types/src/selectors.rs exactly.

Rust Constant Go Constant Selector Key
SELECTOR_CONTEXT_ID SelectorContextID context_id
SELECTOR_CAPABILITY_MASK SelectorCapabilityMask capability_mask
SELECTOR_CORPUS_CID SelectorCorpusCID corpus_cid
SELECTOR_PARAMETERS_CID SelectorParametersCID parameters_cid
SELECTOR_ACCORD_TEMPLATE SelectorAccordTemplate accord_template
SELECTOR_PLAYBOOK SelectorPlaybook playbook
SELECTOR_PRINCIPAL_DID SelectorPrincipalDID principal_did
SELECTOR_DRIVER_ID SelectorDriverID driver_id
SELECTOR_SESSION_MODE SelectorSessionMode session_mode
SELECTOR_SHELL_CLASS SelectorShellClass shell_class
SELECTOR_POSTURE_LEVEL SelectorPostureLevel posture_level

Selector type prefix: gsap. Wire format: gsap:key:value.

Env Var to Selector Mapping

Currently set by bascule-shell and gsh

Source: bascule-oss/crates/bascule-shell/src/main.rs (lines 117190), gsh/gsh/src/human.rs (lines 261262).

Env Var Selector Transform
GSH_DID principal_did Direct. Priority over BASCULE_PRINCIPAL
BASCULE_PRINCIPAL principal_did Direct. Fallback.
BASCULE_AUTH_METHOD driver_id Direct.
BASCULE_POSTURE_LEVEL posture_level Direct (integer string). Priority.
BASCULE_DEFCON_LEVEL posture_level Direct. Fallback.
GSH_CAPABILITY_MASK capability_mask Direct (already hex). Priority.
BASCULE_CAPABILITY_CEILING capability_mask Name → hex via CapabilityCeilingToHex.
BASCULE_SESSION_ID context_id Direct.
BASCULE_CORPUS_CID corpus_cid Direct.

Future (not yet set by any component)

Env Var Selector
GSH_ACCORD_TEMPLATE accord_template
GSH_PLAYBOOK playbook
GSH_PARAMETERS_CID parameters_cid
GSH_SESSION_MODE session_mode
GSH_SHELL_CLASS shell_class

Capability Ceiling Translation

BASCULE_CAPABILITY_CEILING stores a name like CAP_MUTATE. The GSAP selector vocabulary uses hex masks. The ceiling is cumulative ("up to and including"):

Ceiling Name Hex Bits
CAP_NONE 0x00 (none)
CAP_READ 0x01 READ
CAP_PROPOSE 0x03 READ | PROPOSE
CAP_MUTATE 0x07 READ | PROPOSE | MUTATE
CAP_GOVERN 0x0f READ | PROPOSE | MUTATE | GOVERN

Unrecognized ceiling names produce no capability_mask selector (fail-open).

Process Tree Walking Algorithm

Attest(pid):
    current = pid
    for depth = 0; depth < max_depth; depth++:
        env = read /proc/{current}/environ
        if error (process gone):
            return nil, nil  # fail-open

        if isGoverned(env):  # any of BASCULE_PRINCIPAL, GSH_DID,
                             # BASCULE_SESSION_ID, BASCULE_AUTH_METHOD present
            return extractSelectors(env), nil

        parent = read PPid from /proc/{current}/status
        if parent <= 1 or parent == current:
            break
        current = parent

    return nil, nil  # not governed — fail-open

Key properties:

  • No network calls. Reads /proc only.
  • Fail-open. Non-governed processes get zero selectors, not errors.
  • Configurable depth. Default 10 levels. Prevents runaway on deep trees.
  • Race-safe. If a process exits mid-walk, returns nil (not error).

SPIRE Plugin Interface

Uses hashicorp/go-plugin v1.6.3 (same as oidc-attestor).

  • Handshake: ProtocolVersion:1, MagicCookieKey:"ServerAgent", MagicCookieValue:"GuildhouseSpire"
  • Plugin map key: "workload_attestor"
  • Served via plugin.DefaultGRPCServer
  • Loaded as external process via plugin_cmd in SPIRE agent config

SPIRE Registration Entries

For governed shells to receive SVIDs with gsap selectors, create SPIRE entries that match on gsap: selectors:

# Generic governed shell entry
spire-server entry create \
  -spiffeID spiffe://ffc-hetzner-nur01.ops.guildhouse.dev/governed/shell \
  -parentID spiffe://ffc-hetzner-nur01.ops.guildhouse.dev/spire/agent/k8s_psat/ffc-hetzner-nur01/<agent-id> \
  -selector gsap:principal_did:<did> \
  -ttl 3600

# Per-accord-template entries (when GSH_ACCORD_TEMPLATE is available)
spire-server entry create \
  -spiffeID spiffe://ffc-hetzner-nur01.ops.guildhouse.dev/governed/m365-governance \
  -parentID spiffe://ffc-hetzner-nur01.ops.guildhouse.dev/spire/agent/k8s_psat/ffc-hetzner-nur01/<agent-id> \
  -selector gsap:accord_template:m365-governance \
  -ttl 3600

Deployment to Hetzner

Prerequisites

  • SPIRE v1.14.5 running in spire-system namespace (already deployed)
  • gsap-attestor binary built for linux/amd64

Steps

  1. Build the plugin image (adds gsap-attestor to the existing initContainer image):

    podman build -t ghcr.io/guildhouse-cooperative/spire-plugins:latest .
    
  2. Push to registry and update the SPIRE agent DaemonSet's initContainer image tag.

  3. Add to SPIRE agent HCL config:

    WorkloadAttestor "gsap" {
        plugin_cmd = "/opt/spire/plugins/gsap-attestor"
        plugin_data {
            proc_root = "/proc"
            max_depth = 10
        }
    }
    
  4. Restart SPIRE agent DaemonSet to pick up the new plugin.

  5. Verify:

    kubectl -n spire-system logs daemonset/spire-agent | grep gsap
    

File Layout

pkg/gsap/
  selectors.go          # Shared Go constants (mirrors Rust vocabulary)
  selectors_test.go     # Contract tests
cmd/gsap-attestor/
  main.go               # Plugin entry point
  attestor.go           # GsapAttestor: Configure, Attest, extractSelectors
  attestor_test.go      # 28 tests (unit + integration with mock /proc)
  proc.go               # readProcEnviron, getParentPid