Repository navigation
Build a push under an automatic deploy rule on an enforcing Cluster (D3) - #4036
Merged
Merged
Conversation
…rom the attest key
…ecrets from the attest key
…icy, secrets opened only from the attest key
…caller and in the Machine's log
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the
|
…too, before refusing it
… Service in two snapshots, and scope its cache by the attest key
nick-potts
marked this pull request as ready for review
November 9, 2026 15:08
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
On an enforcing Cluster, a push to a rule's approved branch must build without any trust in Ployz Cloud. Cloud must not pick the commit, the inputs, the builder, the runner or the holder, read build secrets, or feed the build its own values. This is slice D3 of
docs/agents/plans/security-0.4/d-plan.md: the Engine half and the builder. The image lands on the grant Machine and is not deployed; deploying is D4.Summary
sequenceDiagram participant R as Runner (pinned builder) participant G as GitHub participant C as Cloud (untrusted) participant M as Grant Machine R->>R: check out github.sha or the commit input, make holder K and recipient R keys R->>C: check-in (OIDC, aud = Cloud origin) C-->>R: signed rule, Cluster key, attest key A, Service, deployment R->>R: freeze deployment once (one snapshot per Service); its build input == rule's policy, or stop R->>G: token, aud = BuildClaim::Grant{holder K, recipient R} R->>C: grant ask {proof, holder, recipient, commit, authority Head} C->>M: MintBuildGrant{rule, proof, Sealed record, reader token} M->>M: signature, Cluster, Machine, ended, Service, holder, claims, aud, head now, SealedDigest, jti once M-->>R: (over iroh, held by K) secrets HPKE-auth-sealed from A to R, info = audience R->>R: open only with A, else stop; build and push R->>G: token, aud = BuildClaim::Built{image, attest A} R->>C: final report + image tokenAdmission order, before anything opens (
core/crates/ployzd/src/machine/local_machine/rule_grant.rs):Builder: getployz/build#8 (draft) adds
.github/workflows/build.yml.BUILDER_SHApins that PR's head,5f35eaa8. If the operator merges it with a merge commit or rebase, that commit stays the pin. A squash merge makes a new commit, and the pin moves to it. Before it merges, the builder'sPLOYZ_URLandPLOYZ_SHA256move from the lab build to the first release that carriesployz build --cloud.Evidence
Unit tests:
cargo nextest run -p ployzd -E 'test(rule) or test(build_grant) or test(oidc)'(65 passed),-p ployz -E 'test(rule) or test(build_grant)',-p ployz-core rule|sealed,-p ployz-build, and theverify-build-handoffmodel check (18 passed, walk unchanged).MintBuildGrant(D1).After: a moved head is refused before the Sealed record opens:
a_rule_mint_refuses_a_moved_head_before_the_sealed_record_opens. A foreign record with the right head reaches the open and fails there as Conflict, so the order is observable.websnapshot with Railpack and its own command. The runner checked the first, built the last.After:
a_second_snapshot_of_the_service_is_refused_before_any_token(fails with the guard removed) anda_deployment_that_swaps_the_recipe_is_refused_before_any_token.ended: true, then generation 3, admitted 3.After:
an_ended_generation_refuses_every_generation_below_itrefuses 3 naming the generation (fails withmark_ruleremoved).After:
a_build_fed_another_attest_key_never_writes_the_clusters_cache_scope. Only the Machine holds the attest key, and it seals (even an empty set) only after admitting the grant, so every build in the Cluster's scope is an admitted one built with the Cluster's own values.Live lanes ran on GitHub (
Ployz-test/lab-service, the builder from a lab fork) against a Boat Cluster with Device enforcement on and a Device-signed rule. A harness stood in for Cloud and relayedMintBuildGrantas Cloud's SDK does, honestly or as an attacker.admitted a rule build's grant rule=baf9c7db… generation=1 service=web commit=085ef9cb…. Rerun on the final daemon: passattackerotherwrote it; main's build ran its RUN step (no hit), then a second main build hit main's own entry (CACHED). The scope now also binds the attest key (unit-tested, not rerun live)Perf: grant admission took 0.13 to 1.8 s per mint, including GitHub's head read. A push took 20 to 25 s to reach the final report. The trunk side of the metric was not measured.
Deviations from d-build-proof
rule: Option<RuleGrant>{rule, proof, service, commit, authority, recipient, reader}. The plan has two fields,ruleandproof.DescribeContract.attest. Cloud relays it at check-in.imageTokenand replaces live step streaming.github_build.rs, Storegithub_authorize, the dashboard routes and the template). No slice before D4 stores a signed rule in Cloud, so check-in has nothing to serve. D4'sgithub rule approveis the first producer. The runner's wire contract is pinned inhandlers/build/rule.rs, and the lab harness implements it..github/workflows/ployz-build.ymlis unchanged. The new file runs on push, and in this repo that starts a failing run on every push to main. The customer copy lives in the builder PR.Bets
MintBuildGrantRequestandContractDescriptiongain optional fields, so old peers ignore them.Residual, under the Cloud security bar: a compromised Cloud can drop, delay or refuse to relay a rule build. It cannot change what gets built.
Merge Danger
Door: two-way
Blast Radius: contained. A rule is used only when a request carries one, and nothing in Cloud sends one yet. Unenforced and legacy builds take the same path as before.