Skip to content

Fleet policy: no compliant publisher-lineage shape, and it accepts ${{ inputs }} in Dagger args #49

Description

@hseshadr

TL;DR

Fleet policy has two gaps, and both are security gaps.

  1. It has no compliant way to verify publisher lineage. edgeproc-core and privacy-core added a run: step to publish.yml that proves the candidate came from a successful release-candidate.yml dispatch on main. Fleet policy reports that step as shell-step, pypi-shape and npm-shape, and privacy-core also gets remote-module-identity and typed-oidc. Publishers without the check (edge-proc, assay) pass.
  2. It accepts a ${{ inputs.* }} expression inside dagger-for-github args. The action pastes args into bash (action.yml at 27b130bf, lines 141 and 177), so assay/.github/workflows/release-candidate.yml:34 (--tag=${{ inputs.tag }}) is script-injectable and passes the policy.

1. Publisher lineage

The publisher job's only gate is workflow_run.head_branch == default_branch. According to edgeproc-core #56 and privacy-core, a workflow_dispatch on a tag named main satisfies that gate. That dispatch runs the tag commit's own release-candidate.yml, so it can upload arbitrary bytes, and the main-branch publish.yml then publishes them through OIDC. I have not reproduced this myself.

The fix in those consumers is a run: step that checks two things with gh api and jq: the triggering run is a successful dispatch of release-candidate.yml in this repo for exactly head_sha, and compare/head_sha...GITHUB_SHA is ahead or identical.

Fleet policy cannot express this today.

  • validate_steps forbids every run: step.
  • The allowed PyPI shapes are (download, pypi) and (download, Dagger, pypi). The only Dagger identity allowed there is github.com/hseshadr/<repo>@${{ github.event.workflow_run.head_sha }}, which is the candidate's own code. In the tag-named-main attack, the attacker writes that code, so a lineage check running in it proves nothing. privacy-core deliberately loads its publisher from @$GITHUB_SHA (the main commit the workflow runs on), and the policy reports that as remote-module-identity.

Proposed direction (needs a decision; not implemented):

  • Add a central, literal-SHA-pinned Dagger function, for example in portfolio-foundation or a new publisher-lineage module in APPROVED_PUBLISHER_MODULES. It takes github-token, run-id, head-sha and main-sha as typed args and fails unless both lineage facts hold.
  • Allow (download, Dagger[approved lineage module], pypi) and require that shape for every publisher. Once every consumer has migrated, reject the bare (download, pypi) shape.
  • Accept a consumer publisher loaded at @${{ github.sha }} in addition to @${{ github.event.workflow_run.head_sha }}, or instead of it.

Until this lands, the publish.yml findings on edgeproc-core (2) and privacy-core (6) are expected. Removing their lineage step to satisfy the policy would reopen the hole.

2. Expressions in Dagger args

Add a finding such as dagger-args-expression. It should reject any ${{ inputs.* }}, ${{ github.event.* }} or ${{ github.head_ref }} in any dagger-for-github with: value. edge-proc#76 and the edgeproc-core and privacy-core fix/fleet-policy-findings PRs show the compliant shape: env: TAG: ${{ inputs.tag }} plus args: ... --tag="$TAG". Their tests run the real args through bash with hostile tags. assay would need the same change.

Found while classifying the 2026-09-25 live fleet scan findings (#48).

🤖 Generated with Claude Code

https://claude.ai/code/session_015oBArfm762nN1r4F4Fst5a

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions