mecatl as its own SPIFFE issuer: trust domain, signing path, bundle publication
@jhrozek is already working on this.
Since Sep 7, 2026.
- Dominant language
- Go
- Stars
- 152
- Forks
- 16
- Avg merge
- 14h 48m
- Merged PRs (30d)
- 536
Description
Scope is the shared infrastructure only — trust domain, signing path, bundle publication. The claim
vocabulary lands with the FS grant service, per the split in #377 ("the signing infrastructure, the
bundle endpoint and the trust domain are shared, and only the claim set differs").
mecatl. First slice of the issuer decision in #377. Gated by #375, which has now answered —
unfavourably for any deployment where a shell is reachable.
Hazards
Facts that were expensive to find and that nobody re-derives on the way to implementing. Each is
verified against code, and the first is verified against a running deployment.
- #375 came back unfavourable, and this issue's payoff is conditional on that answer. #377
sequenced this behind #375 because "if the signing key stays reachable from a shell the model drives
then verifying against it is trusting the harness again." Verified against binaries built from this
repo, in kind, driven through mecatl's own tool surface at--posture=auto: the model reads
mecated's process environment viaBash{cat /proc/1/environ}(PID 1 is mecated;envscrubstrips
the child's env only); reads mecated's memory viadd if=/proc/1/memwith
CapEff: 0000000000000000on a kernel where/proc/sys/kernel/yama/ptrace_scopedoes not exist;
and where a Workload API socket is mounted, obtains mecatl's own SVID and its private key with
spire-agent api fetch x509 -write. So an in-process issuer's day-one payoff — a verifier checking
that a narrowing happened rather than trusting the harness — does not exist while a shell is
reachable. Full answers and evidence tiering on #375. - The pseudo-filesystem guard does not cover Bash, by construction.
internal/app/escapeclassifier.go'sclassifyswitches onRead/Write/Editand returns
escapeInRootfor everything else.Read{/proc/1/environ}is refused with "never relaxed at any
posture"; the same path through Bash is served. The FS tools haveos.Rootplus the guard; the
command runner has a working directory and a scrubbed environment. - The command runner has no OS isolation at all. No
chroot,unshare,seccomp,
SysProcAttrorCredentialanywhere ininternal/adapter/osfs.buildSandboxedCommandRunner's
"sandbox" is a git worktree: it contains writes to the repo, not reads of the host. Anyone reading
the name could reasonably assume otherwise. - Shell-lessness is currently an accident of a base image.
--no-bashdefaults tofalseand
--shellto/bin/shin every binary.cmd/mecak8sis safe today only because its distroless
image has no/bin/sh— verified: it reportsunknown tool "Bash". A base-image change re-opens
all three channels with no flag flipped and no test failing.--no-bashand--shell=""each
remove the tool entirely, verified with a positive control proving the harness still functions. - A process-level SPIRE selector closes the Workload API route, and the chart default undoes it.
Addingunix:path:/usr/local/bin/mecatedto the entry closed it — verified: the shell then received
only the ambient identity, not mecatl's. But the SPIRE Helm chart's defaultClusterSPIFFEIDuses
pod-level selectors (k8s:ns,k8s:pod-label) and issues every pod in the namespace a
ServiceAccount-scoped SVID regardless. Selector hardening has to cover entries nobody on this team
created, and any verifier authorising on trust-domain membership rather than a specific ID is
reachable without an issuer at all. - The trust-domain name is a one-way door. Per #377:
--identity-domainis required when identity
is enabled and validated against the §2.2 grammar (lowercase,[a-z0-9.\-_], at most 255 bytes, no
userinfo or port, no percent-encoding). Never derive it from a bundle-endpoint hostname — the
federation spec gives a concrete subdomain-squatting attack. One domain per deployment; path
prefixes inside one shared domain cannot give key separation because the same key signs both. - The bundle needs one in-house verifier before it needs distribution. #377's open item: the grant
issuer is in-process with the harness but the FS service is a separate process, so a static public
key satisfies it until you want rotation without redeploying verifiers, or a second consumer. - Holder binding has never run. No
cnf, no proof-of-possession, never exercised in any spike. It
is half of what makes a stolen credential useless, and it belongs to the claim-vocabulary slice
rather than this one — recorded here so the split does not lose it.
Acceptance criteria
- Signing material is never held in a process that can spawn a shell. Enforced at startup, not
documented as guidance: either the issuer is off, or Bash is absent. Never both present. -
--identity-domainis required when identity is enabled and validated against the spec grammar.
A name derived from a bundle-endpoint hostname is refused. - The signing intermediate is KMS-rooted, held in memory only, and never written to disk —
including on crash and degrade paths. - Registration pins the workload by binary content (
unix:sha256), not by path alone, and the
deployment asserts that no ambient trust-domain-wide entry grants the agent pod an SVID. - The bundle is published with a documented verifier story for the FS grant service, and rotation
states its overlap window rather than leaving it implicit. - With identity disabled, behaviour and stored bytes are unchanged from today, to the byte.
Proof
The same properties, adversarially, with an actor and an ordering. A property-level criterion can go
green while being false; these are what stop that.
- With a shell reachable, startup refuses to hold signing material, and the refusal names the
reason rather than failing obscurely. - With no shell, an injected agent driving every tool in the catalog obtains no SVID, no key and
no bundle-signing material. Both halves tested — a positive control proving the tools work — so
"the agent found nothing" cannot pass for "there was nothing to find". - A model-spawned process cannot obtain the agent's SVID from the Workload API, with the ambient
ClusterSPIFFEIDpresent exactly as the chart ships it. - A rotation happens while a parked run resumes across it, and the residual validity window is
measured rather than assumed. - A deployment with identity disabled produces byte-identical stored snapshots to one built before
this shipped.
Slices
Decomposition is the implementer's job; these are the natural seams, not a mandate.
- The domain flag and its validation.
- The KMS-rooted signing path, in-memory only.
- Bundle publication and the FS grant service's verifier.
- The startup refusal, and the test that fails if it regresses.
Reasoning
- #377 — the index, where this decision was un-deferred and sequenced behind #375.
- #375 — the spike that gated it, and the evidence for hazard 1.
- #374 — the snapshot MAC key inherits the identical custody problem; the startup-refusal criterion
there and here are the same mechanism. - docs/agent-identity-model.md
and docs/agent-identity-outbound.md
— the design this comes from. - docs/scoped-resource-grants.md
— the consumer that un-deferred this, and whose v1 trust assumption ("the harness and the grant
issuer are trusted; in v1 they are the same process") is the configuration hazard 1 falsifies.
What this issue deliberately does not answer
- The claim vocabulary: instance and child SVID claim sets,
authorization_details, the delegation
chain. Lands with the FS grant service. - Holder binding (
cnf). Never implemented; see hazards. - Whether a broker is needed. That decision turns on where key material lives once the startup
refusal above forces the question, and is tracked on #372.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.