- 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>
58 KiB
Security & Integrity Audit: guildhouse-spire-plugins
Date: 2026-02-18 Commit: eb9edf5 Repository: guildhouse-cooperative/guildhouse-spire-plugins Scope: OIDC token handling, SSH certificate generation, Shellstream extensions, governance integration, key management, configuration/deployment, test fixtures, specification security review License: Apache 2.0 Build Status: UNVERIFIABLE (Go toolchain not available on audit machine)
Attacker Profiles
| ID | Profile | Capabilities |
|---|---|---|
| A1 | Malicious Operator | Valid platform credentials, admin-level Keycloak access, can create intents and ceremonies, can configure SPIRE plugins |
| A2 | Network Attacker | Can intercept, replay, or DoS gRPC traffic between SPIRE Server and Quartermaster services; no valid credentials |
| A3 | Compromised Workload | Has a valid SPIFFE SVID, can call the Workload API, can present crafted OIDC tokens; confined to a single pod/namespace |
| A4 | Rogue Plugin | Binary substituted in the SPIRE plugin path (/opt/spire/plugins/); full access to plugin IPC channel |
| A5 | Insider with Merkle Access | Read/write access to NotaryService data; can query, replay, or forge merkle proofs and anchors |
Severity Scale
| Level | Definition |
|---|---|
| CRITICAL | Exploitable in current code/design; leads to authentication bypass, privilege escalation, or complete audit trail compromise |
| HIGH | Exploitable with moderate effort or upon implementation; leads to significant security degradation |
| MEDIUM | Requires specific conditions or affects defense-in-depth; leads to partial security degradation |
| LOW | Minor issue; limited exploitability or impact |
| INFORMATIONAL | Observation; no direct security impact but worth noting |
| DESIGN-NOTE | Architectural decision with security implications; not a vulnerability per se |
Executive Summary
This audit identified 20 findings: 3 CRITICAL, 6 HIGH, 5 MEDIUM, 2 LOW, 1 INFORMATIONAL, and 3 DESIGN-NOTE.
Top 3 most impactful findings:
-
S-01 (CRITICAL):
CertRequestallows requester-controlled TTL and principals with no server-side enforcement of the spec's 1-hour maximum. A compromised workload can request arbitrarily long-lived certificates with unrestricted principal lists. -
S-02 (CRITICAL): The
Verifierinterface does not contractually require audience validation, enabling token confusion attacks where an OIDC token intended for one service is accepted by another. -
S-03 (CRITICAL): Merkle proofs embedded in SSH certificates are not cryptographically bound to the certificate content, allowing proof replay across certificates to forge governance provenance.
Security posture: The architectural design is sound — fail-closed governance authorization, short-lived certificates, domain-separated hashing, and JCS canonicalization are strong foundations. However, the interface designs in pkg/oidc and pkg/sshcert contain structural flaws that, if carried into implementation, would create exploitable vulnerabilities. The pkg/shellstream implementation is the most mature and has good validation, though it lacks input size limits on the decode path.
Key architectural strengths:
- GovernanceService fail-closed policy (spec Section 10.1) — credential operations cannot bypass authorization
- Short-lived SSH certificates (5m default, 1h max per spec) — limits blast radius of credential compromise
- Domain-separated SHA-256 hashing (
guildhouse.credential.v1:prefix) — prevents cross-protocol collisions - JCS (RFC 8785) canonicalization — ensures deterministic merkle leaf construction
- Append-only merkle chain with
previous_rootlinkage — tamper-evident audit trail
Trust Boundary Diagram
TRUST BOUNDARY: Cluster Network
================================
┌─────────────┐ ┌──────────────────────┐
│ Workload │──[Unix Socket]──▶┌──────────────┐ │ GovernanceService │
│ (Pod) │ Workload API │ SPIRE Agent │──[gRPC/mTLS]──▶ │ (Quartermaster) │
│ │ │ │ SPIRE Server API │ :50051 │
│ OIDC Token │ │ Attestors: │ │ └──────────────────────┘
│ (injected) │ │ - k8s_psat │ │ ▲
└─────────────┘ │ - guildhouse│ ▼ │
│ │ _oidc │┌──────────────┐ │ [gRPC/mTLS]
│ └──────────────┘│ SPIRE Server │ │
│ │ │ ┌──────────────────────┐
│ TRUST BOUNDARY: │ Plugins: │ │ CeremonyService │
│ Pod Filesystem │ ─────────── │ │ (Bascule) │
│ │ KeyManager │──│ :50052 │
▼ │ (substrate) │ └──────────────────────┘
┌─────────────┐ │ │ │
│ token_path │ │ Credential │ │ [gRPC/mTLS]
│ /var/run/ │ │ Composer │ ▼
│ secrets/ │ │ (ssh) │ ┌──────────────────────┐
│ oidc/token │ │ │ │ NotaryService │
└─────────────┘ │ Notifier │──│ (Quartermaster) │
│ (governance)│ │ :50051 │
└──────────────┘ └──────────────────────┘
│
│ [SSH Certificate]
▼
┌──────────────┐
│ SSH Server │
│ (validates │
│ cert + ext) │
└──────────────┘
Trust Boundaries:
TB-1: Workload ↔ SPIRE Agent — Unix domain socket, kernel PID attestation
TB-2: SPIRE Agent ↔ SPIRE Server — gRPC with mTLS (X.509-SVIDs)
TB-3: SPIRE Server ↔ Governance — gRPC, mTLS required (TODO in code)
TB-4: SPIRE Server ↔ Ceremony — gRPC, mTLS required (TODO in code)
TB-5: SPIRE Server ↔ Notary — gRPC, mTLS required (TODO in code)
TB-6: Workload ↔ SSH Server — SSH protocol, certificate-based auth
TB-7: Pod filesystem ↔ Workload — OIDC token file, filesystem permissions
Threat Analysis Matrix
| Finding | Severity | Attacker | Scope Area | Confidentiality | Integrity | Availability |
|---|---|---|---|---|---|---|
| S-01 | CRITICAL | A1, A3 | SSH Cert Generation | Low | High | Low |
| S-02 | CRITICAL | A3 | OIDC Token Handling | High | High | Low |
| S-03 | CRITICAL | A5 | Spec Security | Low | High | Low |
| S-04 | HIGH | A2 | Spec Security | Low | High | Medium |
| S-05 | HIGH | A2, A3 | Shellstream Extensions | Low | Low | High |
| S-06 | HIGH | A1 | Governance Integration | Low | High | Low |
| S-07 | HIGH | A1 | Configuration | Low | Low | High |
| S-08 | HIGH | A1 | OIDC Token Handling | High | High | Low |
| S-09 | HIGH | A4 | Key Management | High | High | High |
| S-10 | MEDIUM | A1, A3 | Governance Integration | Low | Medium | Low |
| S-11 | MEDIUM | A3 | SSH Cert Generation | Low | Medium | Low |
| S-12 | MEDIUM | A1 | Configuration | Low | Low | Medium |
| S-13 | MEDIUM | A1, A5 | Shellstream Extensions | Low | Medium | Low |
| S-14 | MEDIUM | A1, A3 | Test Fixtures | Medium | Medium | Low |
| S-15 | DESIGN-NOTE | A2 | Spec Security | Low | Medium | Low |
| S-16 | LOW | A1 | Spec Security | Low | Medium | Low |
| S-17 | LOW | A5 | Spec Security | Low | Medium | Low |
| S-18 | INFORMATIONAL | A1 | Governance Integration | Low | Medium | Low |
| S-19 | DESIGN-NOTE | A2 | Key Management | Medium | Medium | Low |
| S-20 | DESIGN-NOTE | — | Governance Integration | Low | Low | Low |
Findings
CRITICAL
S-01: CertRequest Allows Requester-Controlled TTL and Principals
Severity: CRITICAL
Attacker Profile: A1 (Malicious Operator), A3 (Compromised Workload)
Scope Area: SSH Certificate Generation
Location: pkg/sshcert/sshcert.go:18-29
Description:
The CertRequest struct exposes two security-critical fields as requester-settable values:
type CertRequest struct {
SpiffeID string
Extensions *shellstream.ShellstreamExtensions
ValidSeconds uint64 // Requester-controlled
Principals []string // Requester-controlled
}
The SSH-SVID specification (specs/spiffe-ssh-svid.md, Section 7.1, lines 390-396) mandates:
- Maximum TTL: 1 hour (MUST NOT exceed)
- Minimum TTL: 30 seconds (MUST NOT issue below)
- Default TTL: 5 minutes (RECOMMENDED)
The Build() method (line 47) performs no TTL bounds checking and no principal validation. The Principals field allows arbitrary SSH principals beyond the SPIFFE ID, which could grant access to hosts or accounts the workload is not authorized to reach.
Attack Scenario:
- A3 obtains a valid SPIFFE SVID through legitimate attestation.
- A3 crafts a
CertRequestwithValidSeconds: 31536000(1 year) andPrincipals: ["root", "admin", "deploy"]. - The SSH Credential Composer builds a certificate with a 1-year lifetime and root/admin principals.
- A3 uses this certificate for persistent, privileged SSH access across the infrastructure.
Impact: Privilege escalation (unrestricted principals), credential persistence beyond intended lifetime (up to max uint64 seconds). Violates the short-lived credential model that is the primary security guarantee of the SSH-SVID design.
Recommendation:
- Add
MaxValidSeconds,MinValidSeconds, andAllowedPrincipalsto theBuilderconfig, enforced server-side. Build()must clampValidSecondsto[MinValidSeconds, MaxValidSeconds]and reject principals not in the allow list.- The SPIFFE ID should be the only principal unless explicitly configured otherwise in the SPIRE registration entry.
S-02: Verifier Interface Lacks Audience Parameter
Severity: CRITICAL
Attacker Profile: A3 (Compromised Workload)
Scope Area: OIDC Token Handling
Location: pkg/oidc/oidc.go:31-34
Description:
The Verifier interface defines:
type Verifier interface {
Verify(ctx context.Context, rawToken string) (*Claims, error)
}
The Config struct (line 10-18) includes Audience string, but this field is never referenced in the Verifier interface contract. An implementation of Verifier has no obligation to check the audience claim. The Claims struct (line 22-28) includes Audience []string in the output, but this is informational — no enforcement occurs.
Additionally:
- No nonce parameter exists (replay protection)
- No JTI (JWT ID) tracking exists (token reuse detection)
Config.Audienceis not validated as required inNewVerifier(line 37-43)
Attack Scenario:
- A3 is a workload in namespace
tenant-bwith a valid OIDC token whoseaudclaim is["api-gateway"]. - A3 presents this token to the SPIRE OIDC attestor, which calls
Verify(ctx, token). - Because audience validation is not part of the interface contract, the implementation may accept the token despite the audience mismatch.
- A3 obtains SPIRE selectors (
oidc:sub:...) and gains an SVID for a workload identity it should not have.
Impact: Token confusion / cross-service authentication bypass. A token issued for one relying party is accepted by another. This is a fundamental OIDC security control.
Recommendation:
- Change the interface to
Verify(ctx context.Context, rawToken string, expectedAudience string) (*Claims, error)to make audience validation contractually required. - Add
audienceas a required parameter that cannot be empty. - Implement JTI tracking with a TTL-bounded cache to prevent token replay.
S-03: Merkle Proof Not Bound to Specific Credential Content
Severity: CRITICAL
Attacker Profile: A5 (Insider with Merkle Access)
Scope Area: Specification Security Review
Location: specs/credential-governance.md:589-599, pkg/shellstream/shellstream.go:69-70
Description:
The Shellstream merkle-proof@guildhouse.dev extension carries a binary inclusion proof, and merkle-root@guildhouse.dev carries the root hash. Together they prove that some leaf was included in the governance merkle tree. However, there is no cryptographic binding between the proof and the specific SSH certificate that carries it.
The merkle leaf is SHA-256(JCS(MutationEnvelope)) (spec Section 7.4, line 360-361). The MutationEnvelope includes payload_hash, actor_svid, tenant_id, event_type, and intent_id — but it does NOT include the SSH certificate's serial number, public key fingerprint, or any certificate-specific identifier.
Attack Scenario:
- A5 observes a legitimately issued certificate C1 with a valid merkle proof P1.
- A5 extracts P1 and the corresponding merkle root R1.
- A5 creates a new certificate C2 (possibly with different principals or TTL) and embeds P1 and R1 as Shellstream extensions.
- A verifier calls
NotaryService.VerifyInclusion(R1, leaf_from_C1, P1)— this succeeds because P1 proves inclusion of C1's leaf, which is still valid. - The verifier concludes C2 has valid governance provenance, but C2 was never actually governed.
Impact: Complete governance audit trail bypass. Certificates can be minted with fabricated governance provenance.
Recommendation:
- The MutationEnvelope MUST include a credential-binding field: the SHA-256 hash of the SSH certificate's public key (or serial number). This binds the merkle leaf to a specific certificate.
- The verifier MUST recompute the leaf hash from the certificate's own content and verify that the proof matches this specific certificate, not just any historical leaf.
- Add the credential binding field to
specs/credential-governance.mdSection 7.3 and theMutationEnvelopeschema.
HIGH
S-04: NotaryService Fail-Open Creates Unaudited Credential Window
Severity: HIGH
Attacker Profile: A2 (Network Attacker)
Scope Area: Specification Security Review
Location: specs/credential-governance.md:661-671
Description:
The specification defines that when the NotaryService is unreachable after a credential operation completes, the credential "MUST proceed" (line 665). This is a fail-open design for the audit channel. The spec requires local queueing and retry (lines 666-668), but the current codebase has no implementation of this queue.
A network attacker who can disrupt connectivity to the NotaryService (TB-5 in the trust boundary diagram) can cause all credential operations to proceed without merkle anchoring. The credentials are valid and usable, but invisible to the audit trail.
Attack Scenario:
- A2 identifies the NotaryService endpoint (
notary.quartermaster.svc.cluster.local:50051per deploy config). - A2 initiates a sustained DoS against the NotaryService or disrupts the network path.
- A1 (colluding or independently) issues credentials during the outage.
- Credentials are issued with valid governance authorization (GovernanceService is fail-closed and still reachable) but no audit trail.
- When the NotaryService recovers, the local queue (if implemented) would eventually anchor the events — but if the queue is lost (pod restart), the events are permanently unaudited.
Impact: Audit gap during NotaryService outage. If combined with local queue loss, permanent audit gap for credential operations during the window.
Recommendation:
- Implement the local durable queue as specified (on-disk WAL or embedded database).
- Add a monitoring metric
governance_anchoring_pending_totalas specified in the spec (line 669). - Consider adding a "maximum anchoring delay" after which credentials should be flagged as requiring re-verification.
- Document the durable queue implementation as a MUST in the implementation guide.
S-05: No Size Limit on Merkle-Proof Base64 Decode
Severity: HIGH
Attacker Profile: A2 (Network Attacker), A3 (Compromised Workload)
Scope Area: Shellstream Extensions
Location: pkg/shellstream/shellstream.go:202-208
Description:
The Decode() function performs base64 decoding on the merkle-proof@guildhouse.dev extension value with no size limit:
if v, ok := extensions[ExtMerkleProof]; ok {
proof, err := base64.StdEncoding.DecodeString(v)
if err != nil {
return nil, fmt.Errorf("shellstream: decode merkle-proof: %w", err)
}
ext.MerkleProof = proof
}
The Shellstream spec (Section 9.4, lines 439-445) recommends a 4 KB total extension size limit, but the code enforces nothing. A crafted SSH certificate with a multi-megabyte merkle-proof value causes unbounded memory allocation when any service calls Decode().
The merkle proof should be at most 8 × 32 = 256 bytes (8 levels × 32-byte SHA-256 hashes) based on the 256-leaf depth limit (spec Section 6.8). In base64, this is ~344 characters.
Attack Scenario:
- A3 crafts an SSH certificate with a
merkle-proof@guildhouse.devextension containing 100 MB of base64-encoded data. - Any service that decodes Shellstream extensions (SSH server middleware, audit verifier, monitoring) calls
Decode(). - The base64 decode allocates ~75 MB of memory per call. Repeated calls exhaust server memory.
Impact: Denial of service against any component that processes Shellstream extensions.
Recommendation:
- Add a size check before decoding:
if len(v) > 512 { return error }(344 base64 chars for max proof, with margin). - Consider adding a total extension payload size check at the start of
Decode()to enforce the 4 KB spec recommendation.
S-06: TOCTOU Between Intent Creation and Credential Issuance
Severity: HIGH
Attacker Profile: A1 (Malicious Operator)
Scope Area: Governance Integration
Location: specs/credential-governance.md:30, pkg/governance/governance.go:53-56
Description:
The governance flow is:
CreateIntent→ Accord policy evaluation → intent authorized or ceremony required- (Optional) Ceremony approval
RedeemIntent→ SAT issued- Credential operation executed with SAT
Between step 1 and step 3, an arbitrary amount of time may pass (minutes for ceremony approval, up to ttl_seconds of the intent). During this window, Accord policy may change. The SAT issued at step 3 reflects the policy evaluation at step 1.
Attack Scenario:
- A1 creates an intent for a credential operation that is currently classified as
Autonomous(no approval needed). - A1 (or another admin) updates the Accord policy to reclassify this operation as
QuorumApproval. - A1 redeems the intent. The GovernanceService issues a SAT based on the original
Autonomousclassification. - The credential is issued without the now-required quorum approval.
Impact: Policy bypass during policy transition windows. The window size equals min(intent_ttl, ceremony_timeout).
Recommendation:
- Re-evaluate Accord policy at
RedeemIntenttime, not just atCreateIntenttime. If the classification has escalated, the intent should transition to the new ceremony requirement. - Alternatively, add a
policy_versionfield to the intent and check at redemption that the policy version hasn't changed. - Document this behavior explicitly in the spec's error handling section.
S-07: GovernanceEpochSeconds Defaults to Zero
Severity: HIGH
Attacker Profile: A1 (Malicious Operator)
Scope Area: Configuration & Deployment
Location: pkg/config/config.go:30
Description:
GovernanceEpochSeconds int `hcl:"governance_epoch_seconds"`
Go initializes int to 0. The Validate() method (lines 34-38) only checks TrustDomain. If governance_epoch_seconds is omitted from configuration, it defaults to 0, which could mean:
- Division by zero in epoch boundary calculations
- Continuous anchoring on every event (resource exhaustion)
- No anchoring at all (implementation-dependent interpretation of epoch=0)
The field comment (line 27-29) documents a default of 300 seconds, but this default is not enforced in code.
Attack Scenario:
- A1 deploys a SPIRE server with a config file that omits
governance_epoch_seconds. - The plugin starts with epoch=0.
- Depending on implementation: epoch boundary triggers on every event (DoS on NotaryService) or never triggers (no anchoring, audit gap).
Impact: Denial of service or silent audit trail failure due to misconfiguration.
Recommendation:
- In
Validate(), checkGovernanceEpochSeconds > 0and set a default of 300 if unset. - Add validation for all required fields:
GovernanceAddr,ClusterID,GovernanceEpochSeconds. - Consider using a pointer type (
*int) to distinguish "not set" from "set to 0".
S-08: OIDC Config Lacks URL Validation for Issuer
Severity: HIGH
Attacker Profile: A1 (Malicious Operator)
Scope Area: OIDC Token Handling
Location: pkg/oidc/oidc.go:37-40
Description:
func NewVerifier(cfg Config) (Verifier, error) {
if cfg.Issuer == "" {
return nil, fmt.Errorf("oidc: issuer is required")
}
// TODO: implement
}
The issuer field is validated only for non-emptiness. OIDC discovery (/.well-known/openid-configuration) fetches the JWKS endpoint from the issuer URL. If the issuer is a file:// URL, http:// URL (no TLS), or points to an attacker-controlled domain, the JWKS keys are attacker-controlled.
Additionally, Config.Audience (line 15) is not validated as required, and Config.JWKSURL (line 18) could override discovery with an arbitrary URL.
Attack Scenario:
- A1 configures the OIDC attestor with
issuer: "http://attacker.example.com". - The attestor fetches
http://attacker.example.com/.well-known/openid-configuration. - The attacker's OIDC discovery document points to their own JWKS endpoint.
- The attacker can now sign any OIDC token that will be accepted by the attestor.
- Workloads in the cluster are attested with attacker-controlled identities.
Impact: Complete OIDC authentication bypass via misconfigured or malicious issuer URL.
Recommendation:
- Validate that
Issueris a well-formedhttps://URL. Rejecthttp://,file://, and non-URL strings. - Validate that
Audienceis non-empty. - If
JWKSURLis set, validate it is alsohttps://. - Consider pinning the expected issuer to a known domain pattern.
S-09: No go-plugin Serving Pattern in Plugin Binaries
Severity: HIGH
Attacker Profile: A4 (Rogue Plugin)
Scope Area: Key Management
Location: cmd/substrate-keymanager/main.go, cmd/ssh-credential-composer/main.go, cmd/governance-notifier/main.go, cmd/oidc-attestor/main.go (all contain func main() { os.Exit(1) })
Description:
SPIRE external plugins use HashiCorp's go-plugin framework, which includes a magic cookie handshake to verify that the plugin binary was launched by the expected host process. Without this handshake:
- Any binary placed at the plugin path will be loaded by SPIRE Server.
- There is no mutual authentication between SPIRE Server and the plugin binary.
- The plugin stub
plugin.gofiles define struct types but have noplugin.Serve()call.
The deploy config (deploy/spire-server-config.yaml, lines 39, 50, 60) specifies absolute plugin paths under /opt/spire/plugins/. If an attacker can write to this path (container escape, misconfigured volume mount, supply chain compromise), they can substitute a rogue binary.
Attack Scenario:
- A4 gains write access to
/opt/spire/plugins/(e.g., via a writablehostPathvolume mount in Kubernetes). - A4 replaces
substrate-keymanagerwith a rogue binary that implements the KeyManager interface. - SPIRE Server loads the rogue binary on next restart.
- The rogue KeyManager has access to all signing keys and can issue arbitrary SVIDs.
Impact: Complete CA key compromise, arbitrary SVID issuance, full trust domain takeover.
Recommendation:
- Implement the
go-pluginserving pattern with the SPIRE-defined handshake config in allcmd/*/main.go. - Use plugin binary checksums in the SPIRE server config (SPIRE supports
plugin_checksum). - Ensure plugin paths are on read-only filesystems in production.
MEDIUM
S-10: RedeemResult Missing ExpiresAt for SAT TTL Enforcement
Severity: MEDIUM
Attacker Profile: A1 (Malicious Operator), A3 (Compromised Workload)
Scope Area: Governance Integration
Location: pkg/governance/governance.go:31-36
Description:
type RedeemResult struct {
Success bool
SatHash []byte
Status string
Error string
}
The proto definition (proto/quartermaster/v1/governance.proto:77-85) includes expires_at in SatToken, but the Go RedeemResult struct does not surface this field. Code consuming RedeemResult has no way to check whether the SAT has expired.
Attack Scenario:
- A credential composer redeems an intent and receives a SAT with a 5-minute TTL.
- Due to processing delay or retry logic, the SAT expires before the credential is issued.
- The composer cannot detect the expiration because
RedeemResultlacksExpiresAt. - The credential is issued with an expired SAT hash embedded in the
sat-hash@guildhouse.devextension.
Impact: Credentials issued with expired authorization tokens. Downstream verifiers that check SAT validity would reject these credentials.
Recommendation:
- Add
ExpiresAt time.TimetoRedeemResult. - Check
time.Now().Before(result.ExpiresAt)before proceeding with credential issuance. - Add
SatBytes []byteto enable downstream SAT verification.
S-11: Roles Decoded Without Validation in Decode Path
Severity: MEDIUM
Attacker Profile: A3 (Compromised Workload)
Scope Area: SSH Certificate Generation
Location: pkg/shellstream/shellstream.go:159-161
Description:
if v, ok := extensions[ExtRoles]; ok && v != "" {
ext.Roles = strings.Split(v, ",")
}
strings.Split("admin,,engineer", ",") produces ["admin", "", "engineer"]. The empty string is silently included in the roles list. While Validate() (lines 247-254) catches this, Decode() and Validate() are separate functions. Any code path that calls Decode() without subsequently calling Validate() operates on unvalidated role data.
Similarly, strings.Split("admin, engineer", ",") produces ["admin", " engineer"] — the leading space is preserved, which could cause authorization mismatches if roles are compared by exact string equality.
Attack Scenario:
- A3 presents an SSH certificate with
roles@guildhouse.dev: "admin,,,". - A server-side authorization check calls
Decode()but notValidate(). - The roles list is
["admin", "", "", ""]— the empty strings may match wildcard or default role rules.
Impact: Authorization bypass if roles are consumed after Decode() without Validate().
Recommendation:
- Filter empty strings and trim whitespace in
Decode()itself, not just inValidate(). - Alternatively, make
Decode()callValidate()internally and return validated data only. - Document that
Decode()output MUST be validated before use.
S-12: Config.Validate() Incomplete — Only Checks TrustDomain
Severity: MEDIUM
Attacker Profile: A1 (Malicious Operator)
Scope Area: Configuration & Deployment
Location: pkg/config/config.go:34-38
Description:
func (c *PluginConfig) Validate() error {
if c.TrustDomain == "" {
return fmt.Errorf("config: trust_domain is required")
}
return nil
}
The PluginConfig struct has 6 fields (lines 10-30) but only TrustDomain is validated. Fields GovernanceAddr, CeremonyAddr, NotaryAddr, ClusterID, and GovernanceEpochSeconds can all be empty or zero without triggering a validation error.
Impact: Plugins start with incomplete configuration and fail at runtime when the first governance call is made, potentially after having already partially processed credentials.
Recommendation:
- Validate all required fields:
GovernanceAddr(non-empty, valid address format),ClusterID(non-empty),GovernanceEpochSeconds(> 0). CeremonyAddrandNotaryAddrshould be validated when the corresponding features are enabled.
S-13: GovernanceIntent Only Format-Validated, Not Existence-Validated
Severity: MEDIUM
Attacker Profile: A1 (Malicious Operator), A5 (Insider with Merkle Access)
Scope Area: Shellstream Extensions
Location: pkg/shellstream/shellstream.go:331-335
Description:
if ext.GovernanceIntent != "" {
if !isValidUUID(ext.GovernanceIntent) {
return fmt.Errorf("shellstream: governance-intent is not a valid UUID: %q", ext.GovernanceIntent)
}
}
The governance-intent@guildhouse.dev extension is validated as a well-formed UUID but there is no mechanism to verify the intent actually exists in the GovernanceService or that it corresponds to the current credential operation.
Attack Scenario:
- A1 crafts a certificate with
governance-intent@guildhouse.dev: "00000000-0000-0000-0000-000000000000". - The UUID is syntactically valid, so
Validate()passes. - A verifier sees the governance-intent field and assumes the credential went through governance.
- But the intent ID is fabricated — no such intent exists in the GovernanceService.
Impact: False governance provenance. Certificates appear governed but were never actually subject to governance authorization.
Recommendation:
- The SSH Credential Composer (at issuance time) must set
governance-intentfrom the actualCreateIntentResponse.intent_id, not from external input. - Verifiers should call
GovernanceService.ListIntentswith the intent ID to confirm existence. - Document that
governance-intentis server-set and MUST NOT be accepted from external sources.
S-14: Sample OIDC Token Contains tenant_id Custom Claim
Severity: MEDIUM
Attacker Profile: A1 (Malicious Operator), A3 (Compromised Workload)
Scope Area: Test Fixtures
Location: test/fixtures/sample-oidc-token.json:21
Description:
{
"tenant_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
The OIDC token fixture includes tenant_id as a top-level custom claim. The deploy config references Keycloak (https://keycloak.guildhouse.example.org/realms/platform), which is self-managed. In Keycloak, custom claims are configurable via protocol mappers and, depending on configuration, may be user-editable (e.g., through a user attribute mapper).
If the OIDC attestor uses tenant_id from the token to determine tenant context (rather than deriving it server-side from the workload's namespace or registration entry), a user who can control their Keycloak attributes can claim any tenant.
Attack Scenario:
- A3 is a workload in
tenant-alphathat has access to modify its own Keycloak user profile. - A3 changes its
tenant_iduser attribute totenant-beta's UUID. - Keycloak issues a token with
"tenant_id": "<tenant-beta-uuid>". - The OIDC attestor extracts
tenant_idfrom the token and provides selectors indicatingtenant-beta. - A3 obtains an SVID with tenant-beta's context.
Impact: Tenant boundary bypass if tenant_id is sourced from the OIDC token.
Recommendation:
- Do NOT use
tenant_idfrom OIDC tokens for authorization decisions. Derive tenant context from the workload's SPIFFE ID path or Kubernetes namespace. - If
tenant_idmust come from OIDC, use a Keycloak hardcoded claim mapper (not user attribute mapper) and restrict mapper editing to admin-only. - Document this trust assumption explicitly.
LOW
S-15: DESIGN-NOTE — Governance-Optional Model (Fail-Closed Auth, Fail-Open Audit)
Severity: DESIGN-NOTE
Attacker Profile: A2 (Network Attacker)
Scope Area: Specification Security Review
Location: specs/credential-governance.md:642-671
Description:
The architecture uses two different failure modes:
- GovernanceService unreachable (Section 10.1, line 644): Fail-CLOSED. Credential operations MUST fail. Authorization is mandatory.
- NotaryService unreachable (Section 10.3, line 661): Fail-OPEN. Credential operations MUST proceed. Audit is best-effort.
This is a deliberate, documented design tradeoff. The rationale (line 671) is that a temporary audit gap is preferable to failing a legitimately authorized credential operation.
Security Implication: An attacker who can selectively disrupt NotaryService connectivity creates a window where credential operations proceed without audit trail entries. This is acceptable in the threat model if:
- The local durable queue (spec line 666) is implemented correctly.
- The queue survives pod restarts.
- Monitoring alerts on
governance_anchoring_pending_total(spec line 669).
Recommendation: No design change needed, but operators must be aware that NotaryService availability directly affects audit completeness. Document this prominently in the deployment guide.
S-16: Emergency Break-Glass Post-Hoc Approval Window Undefined
Severity: LOW
Attacker Profile: A1 (Malicious Operator)
Scope Area: Specification Security Review
Location: specs/credential-governance.md:535-543
Description:
The Accord policy emergency section specifies:
emergency:
classification: EmergencyBreakGlass
post_hoc_approval_window_hours: 24
The spec defines that emergency break-glass ceremonies allow credential operations to proceed before approval, with post-hoc approval required within 24 hours. However, the spec does not define:
- What happens if post-hoc approval is not obtained within the window.
- Whether the credential is automatically revoked.
- Whether subsequent uses of the credential are blocked.
- Whether an alert is generated.
Impact: Emergency credentials could persist indefinitely without retrospective approval. A malicious operator could use emergency break-glass repeatedly as a governance bypass mechanism.
Recommendation:
- Specify in
specs/credential-governance.mdthat credentials issued via EmergencyBreakGlass MUST be automatically revoked if post-hoc approval is not obtained within the window. - Add a required
escalation_actionfield (e.g.,revoke,alert,suspend) to the emergency policy schema.
S-17: Merkle Chain Fork Detection Not Specified
Severity: LOW
Attacker Profile: A5 (Insider with Merkle Access)
Scope Area: Specification Security Review
Location: specs/credential-governance.md:624-630
Description:
The audit chain uses previous_root linkage:
"Each anchor's
previous_rootfield MUST reference themerkle_rootof the immediately preceding anchor." (line 625)
This detects history rewriting (changing an existing anchor) but does not prevent forks: two anchors could both claim the same previous_root, creating a split-brain audit trail. The spec does not define:
- A canonical sequence number for anchors.
- A consensus mechanism for anchor creation.
- Fork detection or resolution procedures.
The CreateAnchorRequest proto (proto/quartermaster/v1/notary.proto:18-22) includes etcd_revision which could serve as a sequence number, but its use is not specified (0 means not set).
Impact: If the NotaryService has multiple writers (e.g., multiple SPIRE servers in an HA deployment), concurrent anchor creation could fork the chain. Each fork is internally consistent but they diverge, making cross-fork audit queries unreliable.
Recommendation:
- Specify that
etcd_revision(or an equivalent monotonic counter) MUST be set and MUST be unique per anchor. - The NotaryService MUST reject
CreateAnchorrequests whereprevious_rootdoes not match the current latest anchor'smerkle_root(optimistic concurrency control). - Document fork detection: auditors should verify that no two anchors share the same
previous_root.
INFORMATIONAL
S-18: CreateIntentRequest identity_claim Oneof Allows Unverifiable External Events
Severity: INFORMATIONAL
Attacker Profile: A1 (Malicious Operator)
Scope Area: Governance Integration
Location: proto/quartermaster/v1/governance.proto:35-38
Description:
oneof identity_claim {
string oidc_token = 5;
ExternalEventClaim external_event = 6;
}
The ExternalEventClaim message (lines 45-50) includes a verification string field with no defined verification protocol:
message ExternalEventClaim {
string source = 1;
string event_id = 2;
string event_type = 3;
string verification = 4;
}
The GovernanceService must decide whether to trust an external event claim, but there is no standard for what verification should contain (HMAC, signature, URL to verify, etc.).
Impact: If the GovernanceService accepts external event claims without strong verification, a malicious operator can forge identity claims to create intents on behalf of other actors.
Recommendation:
- Define a verification protocol for external event claims (e.g., HMAC with a shared secret, or a URL that returns verification status).
- Add a
verification_typefield toExternalEventClaimto disambiguate verification methods. - Consider requiring that external event claims always trigger at least
SingleApprovalceremony classification.
DESIGN-NOTE
S-19: No mTLS Requirement in gRPC Client Configuration
Severity: DESIGN-NOTE
Attacker Profile: A2 (Network Attacker)
Scope Area: Key Management
Location: pkg/governance/governance.go:44-49
Description:
func NewClient(cfg Config) (*Client, error) {
if cfg.GovernanceAddr == "" {
return nil, fmt.Errorf("governance: governance address is required")
}
// TODO: implement — establish gRPC connections with mTLS
return &Client{config: cfg}, nil
}
The Config struct (lines 10-20) has GovernanceAddr, CeremonyAddr, and NotaryAddr but no TLS configuration fields (certificate paths, CA bundle, TLS required flag). The TODO comment mentions mTLS, but when this is implemented, if mTLS is made optional rather than required, a network attacker can intercept governance traffic.
Impact: If mTLS is not enforced, all trust boundaries TB-3, TB-4, and TB-5 in the trust diagram are vulnerable to man-in-the-middle attacks.
Recommendation:
- Add
TLSCertPath,TLSKeyPath,TLSCAPathto the governanceConfigstruct. - Make TLS mandatory —
NewClientshould fail if TLS configuration is missing. - Use SPIFFE-aware TLS (SVID-based mTLS) for consistency with the SPIRE ecosystem.
S-20: Proto Files Lack Field Validation Annotations
Severity: DESIGN-NOTE
Attacker Profile: N/A
Scope Area: Governance Integration
Location: All proto files under proto/
Description:
None of the 4 proto files use field validation annotations (e.g., buf/validate or protoc-gen-validate). Fields with implicit constraints have no documentation of valid ranges:
| Proto File | Field | Expected Constraint |
|---|---|---|
governance.proto:40 |
ttl_seconds |
> 0, <= max_intent_ttl |
governance.proto:41 |
max_redemptions |
> 0, reasonable upper bound |
ceremony.proto:43 |
required_approvals |
> 0, <= pool_size |
ceremony.proto:44 |
ttl_hours |
> 0, reasonable upper bound |
notary.proto:21 |
etcd_revision |
>= 0 |
Impact: Generated client/server code has no built-in validation. All validation must happen in application code, increasing the risk of missing checks.
Recommendation:
- Add
buf/validateannotations to proto fields with constraints. - This provides both documentation and generated validation code.
- At minimum, add proto-level comments documenting valid ranges for each constrained field.
Attack Trees
Attack Tree 1: Forge SSH Certificate with Arbitrary Privileges
GOAL: Obtain SSH certificate with unauthorized privileges
├── 1. Exploit CertRequest TTL/Principal control [S-01] [CRITICAL]
│ ├── 1a. Compromised workload (A3) calls credential composer
│ │ ├── Set ValidSeconds = 31536000 (1 year)
│ │ ├── Set Principals = ["root", "deploy", "admin"]
│ │ └── RESULT: Long-lived cert with privileged principals
│ └── 1b. Malicious operator (A1) configures registration entry
│ ├── Set overly broad principal mapping
│ └── RESULT: Legitimate-looking but over-privileged certs
│
├── 2. Bypass OIDC attestation [S-02, S-08]
│ ├── 2a. Token confusion (A3) [S-02]
│ │ ├── Present token with wrong audience
│ │ ├── Verifier lacks audience check
│ │ └── RESULT: SVID for unintended workload identity
│ └── 2b. Malicious issuer (A1) [S-08]
│ ├── Configure issuer = "http://attacker.example.com"
│ ├── Serve attacker-controlled JWKS keys
│ └── RESULT: Arbitrary identity attestation
│
└── 3. Fabricate governance provenance [S-03, S-13]
├── 3a. Replay merkle proof (A5) [S-03]
│ ├── Extract proof from legitimate cert C1
│ ├── Embed in forged cert C2
│ └── RESULT: C2 appears governed
└── 3b. Fabricate intent ID (A1) [S-13]
├── Set governance-intent = random valid UUID
├── Passes format validation
└── RESULT: Cert appears to have governance intent
Attack Tree 2: Bypass Governance for Credential Issuance
GOAL: Issue credentials without proper governance authorization
├── 1. Exploit TOCTOU window [S-06] [HIGH]
│ ├── 1a. Create intent under permissive policy (A1)
│ │ ├── Intent classified as Autonomous
│ │ ├── Policy updated to require QuorumApproval
│ │ ├── Redeem intent (still uses old classification)
│ │ └── RESULT: Credential issued without required quorum
│ └── 1b. Race condition during policy deployment
│ ├── Multiple intents created during transition
│ └── RESULT: Batch of under-governed credentials
│
├── 2. Suppress audit trail [S-04, S-15]
│ ├── 2a. DoS NotaryService (A2) [S-04]
│ │ ├── NotaryService unreachable
│ │ ├── Credentials proceed (fail-open audit)
│ │ ├── Local queue lost on pod restart
│ │ └── RESULT: Permanently unaudited credential operations
│ └── 2b. Exploit anchoring delay
│ ├── Issue credentials faster than epoch boundary
│ ├── Exceed 256 leaves per epoch [spec depth limit]
│ └── RESULT: Leaves dropped or epoch overflow
│
└── 3. Exploit emergency break-glass [S-16]
├── Trigger emergency condition (A1)
├── Credential issued without pre-approval
├── Post-hoc approval window expires
├── No automatic revocation specified
└── RESULT: Ungoverned credential persists indefinitely
Attack Tree 3: Corrupt Audit Trail
GOAL: Compromise integrity of the governance audit trail
├── 1. Proof replay attack [S-03] [CRITICAL]
│ ├── 1a. Cross-credential replay (A5)
│ │ ├── Extract (merkle-root, merkle-proof) from cert C1
│ │ ├── Embed in crafted cert C2
│ │ ├── VerifyInclusion succeeds (proves C1's leaf, not C2's)
│ │ └── RESULT: C2 has false governance provenance
│ └── 1b. Cross-epoch replay
│ ├── Use proof from epoch N in cert issued at epoch N+1
│ ├── Governance-epoch mismatch may not be checked
│ └── RESULT: Stale governance state appears current
│
├── 2. Fork the merkle chain [S-17] [LOW]
│ ├── 2a. Concurrent anchor creation (A5)
│ │ ├── Two writers create anchors with same previous_root
│ │ ├── Chain forks into two valid branches
│ │ └── RESULT: Audit queries return inconsistent results
│ └── 2b. Reorder anchors
│ ├── etcd_revision = 0 (not set) per proto comment
│ ├── No sequence number to enforce ordering
│ └── RESULT: Anchors may be out of causal order
│
├── 3. DoS-induced audit gap [S-04, S-05]
│ ├── 3a. Overwhelm extension decoder (A3) [S-05]
│ │ ├── Craft cert with 100MB merkle-proof
│ │ ├── Audit verifier calls Decode()
│ │ ├── OOM kills verifier process
│ │ └── RESULT: Audit verification unavailable
│ └── 3b. NotaryService DoS (A2) [S-04]
│ ├── Sustained DoS during credential operations
│ └── RESULT: Audit gap for all operations during outage
│
└── 4. Fabricate governance metadata [S-13, S-18]
├── 4a. Fake intent ID [S-13]
│ ├── governance-intent = random UUID
│ └── RESULT: Cert appears governed when it wasn't
└── 4b. Forge external event claim [S-18]
├── CreateIntent with fabricated ExternalEventClaim
├── GovernanceService may accept without strong verification
└── RESULT: Intent created under false identity
Scope Area Deep Dives
1. OIDC Token Handling
Findings: S-02 (CRITICAL), S-08 (HIGH), S-14 (MEDIUM)
Files reviewed: pkg/oidc/oidc.go, cmd/oidc-attestor/plugin.go, test/fixtures/sample-oidc-token.json
The OIDC subsystem has three compounding issues:
- The
Verifierinterface (S-02) does not contractually require audience validation. - The issuer URL (S-08) is not validated for HTTPS.
- The
tenant_idclaim (S-14) may be user-controllable in Keycloak.
Together, these create a layered authentication failure: even if one control is implemented correctly, the others provide bypass routes. The recommended fix order is: S-02 first (interface design, hardest to change later), then S-08 (configuration validation), then S-14 (trust model documentation).
2. SSH Certificate Generation
Findings: S-01 (CRITICAL), S-11 (MEDIUM)
Files reviewed: pkg/sshcert/sshcert.go, cmd/ssh-credential-composer/plugin.go, specs/spiffe-ssh-svid.md
The SSH certificate generation path has a fundamental interface design flaw (S-01) where the CertRequest struct allows callers to control security-critical fields. The SSH-SVID spec clearly defines TTL bounds (30s–1h) and principal derivation (from SPIFFE ID), but the code enforces neither. When this stub is implemented, the Build() method MUST enforce these constraints server-side.
3. Shellstream Extensions
Findings: S-05 (HIGH), S-13 (MEDIUM)
Files reviewed: pkg/shellstream/shellstream.go, pkg/shellstream/shellstream_test.go, specs/shellstream-extensions.md
The Shellstream implementation is the most mature code in the repository (360 lines, extensive tests). The primary security concern is the lack of size limits on the decode path (S-05), which is a straightforward DoS vector. The spec recommends a 4 KB total extension limit (Section 9.4, line 441-445), and the merkle proof has a known maximum size of ~256 bytes — both provide clear bounds for validation.
The governance-intent format-only validation (S-13) is a design issue: the extension value looks correct syntactically but could be fabricated. This is mitigated if the credential composer sets it server-side and verifiers cross-check with the GovernanceService.
4. Governance Integration
Findings: S-06 (HIGH), S-07 (HIGH), S-10 (MEDIUM), S-18 (INFORMATIONAL), S-20 (DESIGN-NOTE)
Files reviewed: pkg/governance/governance.go, pkg/config/config.go, proto/quartermaster/v1/governance.proto, specs/credential-governance.md
The governance integration has the widest attack surface. The TOCTOU window (S-06) is an inherent challenge in any async authorization system with separate create/redeem steps. The spec should either re-evaluate policy at redemption time or bound the intent TTL to minimize the window. The GovernanceEpochSeconds default-to-zero (S-07) is a dangerous misconfiguration vector. The RedeemResult missing ExpiresAt (S-10) prevents SAT TTL enforcement in consumer code.
5. Key Management
Findings: S-09 (HIGH), S-19 (DESIGN-NOTE)
Files reviewed: cmd/substrate-keymanager/main.go, cmd/substrate-keymanager/plugin.go, pkg/governance/governance.go
Key management is entirely scaffolded. The most significant risk is the absence of the go-plugin serving pattern (S-09), which means there is no binary trust verification between SPIRE Server and the plugin. The mTLS requirement (S-19) for gRPC connections to Quartermaster services is noted in TODOs but not yet structurally enforced.
6. Configuration & Deployment
Findings: S-07 (HIGH), S-08 (HIGH), S-12 (MEDIUM)
Files reviewed: pkg/config/config.go, pkg/oidc/oidc.go, deploy/spire-server-config.yaml, deploy/spire-agent-config.yaml
Configuration validation is the weakest link in the current codebase. The PluginConfig.Validate() method checks only one of six fields (S-12). The oidc.NewVerifier checks only issuer non-emptiness (S-08). The GovernanceEpochSeconds field defaults to zero with no validation (S-07). These gaps mean that misconfigured deployments will start successfully but fail in unpredictable ways at runtime.
7. Test Fixtures
Findings: S-14 (MEDIUM)
Files reviewed: test/fixtures/sample-oidc-token.json, test/fixtures/sample-ssh-cert-extensions.json
The OIDC token fixture includes a tenant_id custom claim that, in a self-managed Keycloak deployment, could be user-controllable. This is primarily a trust model documentation issue — the fixture itself is correct for testing, but the trust assumption about where tenant_id comes from must be made explicit in the implementation.
8. Specification Security Review
Findings: S-03 (CRITICAL), S-04 (HIGH), S-15 (DESIGN-NOTE), S-16 (LOW), S-17 (LOW)
Files reviewed: All 3 specs — specs/spiffe-ssh-svid.md (Section 8), specs/shellstream-extensions.md (Section 9), specs/credential-governance.md (Sections 9-10)
The specifications are well-written with dedicated security sections. The SSH-SVID spec (Section 8, 7 subsections) covers attestation trust, SPIFFE ID visibility, mTLS, socket protection, rate limiting, CA key compromise, and clock skew. The Shellstream spec (Section 9, 5 subsections) covers extension visibility, authorization boundary, parsing safety, size constraints, and replay/freshness. The Credential Governance spec (Section 10, 5 subsections) covers fail-closed/fail-open modes, ceremony timeout, and idempotency.
The critical gap is the merkle proof binding (S-03): the proof is not tied to the specific certificate. The other spec-level findings (S-16, S-17) are edge cases in the emergency and chain fork scenarios that should be addressed but are not urgent.
Appendix A: Attacker Profile × Finding Matrix
Cells marked with severity initial: C=CRITICAL, H=HIGH, M=MEDIUM, L=LOW, I=INFO, D=DESIGN-NOTE. Empty = not exploitable by this profile.
| Finding | A1 Malicious Operator | A2 Network Attacker | A3 Compromised Workload | A4 Rogue Plugin | A5 Merkle Insider |
|---|---|---|---|---|---|
| S-01 | C | C | |||
| S-02 | C | ||||
| S-03 | C | ||||
| S-04 | H | ||||
| S-05 | H | H | |||
| S-06 | H | ||||
| S-07 | H | ||||
| S-08 | H | ||||
| S-09 | H | ||||
| S-10 | M | M | |||
| S-11 | M | ||||
| S-12 | M | ||||
| S-13 | M | M | |||
| S-14 | M | M | |||
| S-15 | D | ||||
| S-16 | L | ||||
| S-17 | L | ||||
| S-18 | I | ||||
| S-19 | D | ||||
| S-20 |
Summary by attacker profile:
- A1 (Malicious Operator): 9 findings exploitable. Highest concentration of risk.
- A2 (Network Attacker): 3 findings exploitable. Primary vector is service DoS.
- A3 (Compromised Workload): 5 findings exploitable. Primary vector is cert/token manipulation.
- A4 (Rogue Plugin): 1 finding exploitable. High impact but narrow vector.
- A5 (Merkle Insider): 3 findings exploitable. Primary vector is audit trail manipulation.
Appendix B: Trust Boundary Inventory
| ID | Boundary | From | To | Protocol | Authentication | Encryption | Status |
|---|---|---|---|---|---|---|---|
| TB-1 | Workload API | Workload | SPIRE Agent | Unix Socket | Kernel PID attestation | N/A (local) | Specified in SSH-SVID spec Section 8.4 |
| TB-2 | SPIRE Node API | SPIRE Agent | SPIRE Server | gRPC | mTLS with X.509-SVIDs | TLS 1.2+ | Specified in SSH-SVID spec Section 8.3 |
| TB-3 | Governance API | SPIRE Server | GovernanceService | gRPC | mTLS (TODO) | TLS (TODO) | S-19: No TLS config in code |
| TB-4 | Ceremony API | SPIRE Server | CeremonyService | gRPC | mTLS (TODO) | TLS (TODO) | S-19: No TLS config in code |
| TB-5 | Notary API | SPIRE Server | NotaryService | gRPC | mTLS (TODO) | TLS (TODO) | S-19: No TLS config in code |
| TB-6 | SSH Auth | Workload | SSH Server | SSH | Certificate-based | SSH encryption | Spec-compliant (standard OpenSSH) |
| TB-7 | Token File | Pod filesystem | Workload | File I/O | Filesystem permissions | N/A | Specified in agent config: /var/run/secrets/oidc/token |
| TB-8 | Plugin IPC | SPIRE Server | Plugin Binary | go-plugin/gRPC | Magic cookie handshake (TODO) | Local | S-09: No handshake implemented |
Appendix C: Specification Security Coverage
SSH-SVID Specification (specs/spiffe-ssh-svid.md, Section 8)
| Topic | Covered | CWE Reference | Assessment |
|---|---|---|---|
| Attestation trust | Section 8.1 | CWE-287 (Improper Authentication) | Thorough. Recommends k8s attestation, warns about PID-based. |
| Identity visibility | Section 8.2 | CWE-200 (Exposure of Sensitive Information) | Good. Notes SPIFFE ID in cleartext during SSH handshake. |
| Transport security | Section 8.3 | CWE-319 (Cleartext Transmission) | Good. Requires mTLS for Agent-Server. |
| Socket protection | Section 8.4 | CWE-732 (Incorrect Permission Assignment) | Good. Requires filesystem permissions + kernel attestation. |
| Rate limiting | Section 8.5 | CWE-770 (Allocation Without Limits) | Good. Recommends 60 issuances/min/SPIFFE-ID. |
| CA compromise | Section 8.6 | CWE-321 (Use of Hard-coded Cryptographic Key) | Good. Recommends HSM, monitoring, rotation. |
| Clock skew | Section 8.7 | CWE-613 (Insufficient Session Expiration) | Good. 60-second tolerance. |
| Certificate TTL enforcement | NOT COVERED | CWE-613 | GAP: Spec defines bounds (S-01) but no server-side enforcement guidance. |
| Principal restriction | NOT COVERED | CWE-269 (Improper Privilege Management) | GAP: Spec derives principal from SPIFFE ID but doesn't restrict additional principals. |
Shellstream Extensions Specification (specs/shellstream-extensions.md, Section 9)
| Topic | Covered | CWE Reference | Assessment |
|---|---|---|---|
| Extension visibility | Section 9.1 | CWE-200 | Good. "MUST NOT embed secrets." |
| Authorization boundary | Section 9.2 | CWE-863 (Incorrect Authorization) | Excellent. "Merkle proofs provide auditability, not authorization." |
| Parsing safety | Section 9.3 | CWE-94 (Improper Control of Code Generation) | Good. "MUST NOT evaluate as code." |
| Size constraints | Section 9.4 | CWE-770 | Partial. Recommends 4 KB limit but uses SHOULD, not MUST. |
| Replay/freshness | Section 9.5 | CWE-294 (Authentication Bypass by Capture-replay) | Partial. Relies on cert validity period. No proof-to-cert binding. |
| Proof binding | NOT COVERED | CWE-345 (Insufficient Verification of Data Authenticity) | GAP: S-03 — proof is not bound to specific certificate content. |
Credential Governance Specification (specs/credential-governance.md, Sections 9-10)
| Topic | Covered | CWE Reference | Assessment |
|---|---|---|---|
| Merkle anchoring | Section 9.1 | CWE-354 (Improper Validation of Integrity Check) | Good. Detailed merkle construction with domain separation. |
| Certificate-embedded audit | Section 9.2 | CWE-778 (Insufficient Logging) | Good. Defines required extensions. |
| Audit queries | Section 9.3 | CWE-778 | Good. Three verification patterns defined. |
| Chain continuity | Section 9.4 | CWE-354 | Good. Append-only with previous_root linkage. |
| Fail-closed auth | Section 10.1 | CWE-636 (Not Failing Securely) | Excellent. GovernanceService unreachable = operation fails. |
| Ceremony timeout | Section 10.2 | CWE-613 | Good. Timeout = denial. |
| Fail-open audit | Section 10.3 | CWE-778 | Acceptable tradeoff, well-documented rationale. |
| Policy missing | Section 10.4 | CWE-636 | Good. Default to SingleApproval. |
| TOCTOU | NOT COVERED | CWE-367 (TOCTOU Race Condition) | GAP: S-06 — policy re-evaluation at redemption not specified. |
| Chain fork | NOT COVERED | CWE-362 (Concurrent Execution Using Shared Resource) | GAP: S-17 — no fork prevention mechanism. |
| Break-glass expiry | PARTIAL | CWE-613 | GAP: S-16 — post-hoc approval expiry action undefined. |
End of security audit report.