Skip to content

ci: run trusted Linux x86-64 checks on Namespace - #120

Closed
andersalm wants to merge 2 commits into
feat/0.7.1-modelsfrom
fix/0.7.1-namespace-ci
Closed

andersalm wants to merge 2 commits into
feat/0.7.1-modelsfrom
fix/0.7.1-namespace-ci

Conversation

@andersalm

@andersalm andersalm commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Part of #114

Trusted Linux x86-64 pull requests use Namespace. ARM64, fork pull requests, macOS and release publication stay on GitHub runners. Check names, tests, time limits and GitHub caches stay the same.

On the same tested source, the x86-64 source-home job took 25m36s on Namespace and 43m16s on GitHub. Namespace ARM64 exceeded 90 minutes; GitHub ARM64 passed in 36m27s. This PR therefore keeps ARM64 on GitHub. Full timings and comparison limits are in #114.

The manual runner option supports a GitHub comparison on the same source. Release publication runs only on a tag push. The change targets feat/0.7.1-models and depends on that branch.

Validation: actionlint 1.7.12, YAML/routing checks, Home entropy, Rust formatting, diff checks and independent source review passed. Final candidate CI passed all eight checks at 8cc32664 (run 36618604940). Independent source review passed. Ready for Irzhy's review; Anders contacts him.

Merge gate: verify that Namespace itself refuses fork PR jobs, including a fork that changes runner labels. GitHub approval for external workflows does not establish this. Keep this PR open until that check passes. The x86-64 gain is faster checks and earlier failure results; macOS remained the 45–50 minute critical path in the comparison.

@andersalm
andersalm requested a review from irzhywau September 29, 2026 17:32
@andersalm andersalm added this to the 0.7.1 milestone Sep 29, 2026
@andersalm andersalm changed the title ci: run trusted Linux checks on Namespace ci: run trusted Linux x86-64 checks on Namespace Sep 29, 2026
@andersalm
andersalm removed the request for review from irzhywau September 29, 2026 19:33
@andersalm
andersalm marked this pull request as ready for review September 29, 2026 20:06
@andersalm
andersalm requested a review from irzhywau September 29, 2026 20:51
@andersalm

Copy link
Copy Markdown
Contributor Author

@irzhywau ready for your review. CI only: trusted Linux x86-64 jobs move to Namespace; ARM64, macOS, fork PRs and publishing stay on GitHub. Focus: can a fork PR or a manual run reach a Namespace runner or the release job? It merges only after proof from Namespace's side that it refuses fork jobs (tracked in #114).

@andersalm andersalm mentioned this pull request Sep 29, 2026
9 tasks
@irzhywau

Copy link
Copy Markdown
Collaborator

Security review — #120 ci: run trusted Linux x86-64 checks on Namespace

@andersalm — security review of head 8cc32664 against feat/0.7.1-models. Review only; no changes were made to the branch.

Verdict: changes requested. Two blockers (F1, F2) before merge; F3–F4 are small and belong in the same PR.

Severity scale: Critical exploitable now with direct impact · High realistic path to code execution or artifact integrity loss · Medium needs an extra condition (infra compromise, maintainer error) · Low hardening / drift risk · Info no action.


F1 — High · The fork boundary is not enforced by the workflow (merge gate)

Where: every runs-on: expression (.github/workflows/ci.yml:39, 79, 103, 147, 203, 303).

Threat. For pull_request events GitHub runs the workflow file from the PR's merge commit, i.e. the fork's own copy of ci.yml. A fork can replace the expression with runs-on: namespace-profile-elastos-ci-x86 and the head.repo.full_name == github.repository check never executes. This is true as soon as the Namespace GitHub App is installed on the repository, independent of this PR. The repository is public, so this is the "self-hosted runners on a public repo" case GitHub warns about.

Today the only barrier is the fork-approval policy (all_external_contributors): one maintainer clicking Approve and run without reading the workflow diff routes attacker code onto Namespace.

What an attacker gains depends on the Namespace profile:

  • Persistent state (cache volumes / shared disks): poison caches reused by trusted runs → code execution in later trusted jobs. This is the serious case.
  • Cloud credentials / OIDC trust attached to the profile → lateral movement.
  • Otherwise: compute-cost abuse (mining) on the org's Namespace bill.

Fix / acceptance criteria for the owner:

  1. Keep the merge gate stated in the PR body: demonstrate that Namespace refuses a job from a fork PR whose ci.yml hard-codes namespace-profile-elastos-ci-x86. Record the run link.
  2. Independently of (1), document in the PR (or 0.7.1: Run trusted Linux CI on Namespace #114) that the elastos-ci-x86 profile has:
    • ephemeral instances only (one job per machine);
    • no Namespace cache volumes / persistent disks;
    • no cloud credentials, no OIDC trust relationship.
  3. If (1) cannot be shown and the profile needs cache volumes → do not merge; keep PRs on GitHub runners.
  4. Keep the repo setting "Require approval for all external contributors" on; note it in docs/ as a dependency of this routing.

F2 — High (Medium likelihood) · Namespace-built objects can flow into GitHub-built release binaries

Where:

  • ci.yml:14 — linux_runner defaults to namespace;
  • ci.yml:327-332 — source-home-linux uses Swatinem/rust-cache@v2 with prefix-key: ${{ runner.os }}-${{ runner.arch }}-elastos-bd and cache-directories: target-build;
  • .cargo/config.toml — build-dir = "target-build" holds all dependency rlibs, build-script outputs and fingerprints;
  • ci.yml:381-384 — the same job uploads the release tarballs consumed by publish-github-release.

Path.

  1. Someone runs a manual dispatch on main (default linux_runner: namespace) → source-home-linux (x86) runs on Namespace and saves its cache in the main scope. The key is identical to the GitHub-hosted one (Linux-X64-elastos-bd-…; same job id, toolchain and lockfile).
  2. A v* tag push runs on GitHub-hosted runners, and tag runs can restore caches from the default branch.
  3. Cargo treats registry-dependency artifacts as fresh by fingerprint (their mtime is not rechecked), so dependency rlibs and build-script outputs compiled on Namespace are linked into the published release binary. Workspace crates rebuild after checkout, but dependencies do not.

Threat. A compromise of the third-party runner infrastructure, or of any job running in main scope on it, becomes a supply-chain injection into signed or published releases. It breaks the invariant that release artifacts are built only on GitHub-hosted infrastructure from reviewed source. The PR body's claim that tag/main publication "stays on GitHub" holds for where the job runs, not for what it restores.

The same key-sharing exists for ci.yml:89-94, 111-127, 157-176 and 261-279 (lint/tests/custody smoke, including the buildx type=gha layer cache). None of those feed a release, so they matter less, but the fix should cover them uniformly.

Fix (recommended):

  1. Make release builds cache-free: on the Swatinem/rust-cache step of source-home-linux (and source-home-macos for symmetry), add if: github.ref_type != 'tag'. Tag builds then compile cold, which costs time only on releases.
  2. Separate cache namespaces per runner provider so a GitHub-hosted job can never restore something Namespace wrote. Add the provider to every cache key:
    - uses: Swatinem/rust-cache@v2
      if: github.ref_type != 'tag'
      with:
        workspaces: elastos
        prefix-key: ${{ runner.os }}-${{ runner.arch }}-${{ needs.route.outputs.provider }}-elastos-bd
        cache-directories: target-build
    Apply the same provider segment to: rust-cache prefix-key at lines 92, 114, 163, 264, 330, 411; actions/cache keys at 127, 176, 422; and the buildx cache (--cache-from/--cache-to type=gha,scope=${{ needs.route.outputs.provider }}-custody).
    needs.route.outputs.provider comes from F4.
  3. Cheaper alternative, if (2) is too invasive: save-if: ${{ github.event_name == 'push' }} on every cache step, so only GitHub-hosted push runs ever write caches. PR re-runs become slower.

Acceptance: show a tag-like dispatch (or a dry-run on a test tag in a fork) whose source-home-linux log reports no cache restored. Show that a Namespace PR run's cache key contains namespace and a GitHub run's contains github.


F3 — Low · Manual dispatch defaults to the third-party runner

Where: ci.yml:10-14.

Threat. The default for the most privileged manual path (dispatch on main, with an arbitrary ref input) is the less-trusted runner, and it is what enables the F2 path. It is surprising for an operator who just wants to "re-run CI on main".

Fix: default: github. Namespace becomes an explicit opt-in for the #114 comparison. No other change needed.


F4 — Low · Routing policy duplicated in six places

Where: the identical long expression at ci.yml:39, 79, 103, 147, 203 and a variant at 303.

Threat. A future edit that updates five of six copies silently routes one job (possibly a new release-relevant job) to the wrong runner. Security policy should have one source of truth.

Fix: add a tiny first job and consume its outputs:

route:
  runs-on: ubuntu-24.04
  timeout-minutes: 2
  outputs:
    provider: ${{ steps.r.outputs.provider }}
    linux_x86: ${{ steps.r.outputs.linux_x86 }}
  steps:
    - id: r
      env:
        TRUSTED: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || (github.event_name == 'workflow_dispatch' && github.ref_type != 'tag' && inputs.linux_runner == 'namespace') }}
      run: |
        if [ "$TRUSTED" = "true" ]; then
          echo "provider=namespace" >> "$GITHUB_OUTPUT"
          echo "linux_x86=namespace-profile-elastos-ci-x86" >> "$GITHUB_OUTPUT"
        else
          echo "provider=github" >> "$GITHUB_OUTPUT"
          echo "linux_x86=ubuntu-24.04" >> "$GITHUB_OUTPUT"
        fi

Then source-gate gets needs: route and runs-on: ${{ needs.route.outputs.linux_x86 }}. Every downstream job adds route to needs. For source-home-linux, keep the matrix but select matrix.os == 'ubuntu-24.04' && needs.route.outputs.linux_x86 || matrix.os. Pass values via env: as shown, never by interpolating into run:.


Positive findings (keep)

  • Info: publish-github-release now requires github.event_name == 'push' (ci.yml:489). Previously a manual dispatch on a v* tag ref could publish a release. This is a real hardening.
  • Info: ARM64, macOS, main pushes, tag pushes and fork PRs stay on GitHub-hosted runners as intended by the YAML.
  • Info: no secrets.* are used by any Namespace-routable job; the workflow token stays contents: read (plus pull-requests: read for the custody filter).

Out of scope (existing, not introduced here)

  • Third-party actions are pinned by tag, not SHA (softprops/action-gh-release@v2 runs with contents: write). Consider SHA pinning in a separate PR.

@irzhywau irzhywau left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please see comments

@andersalm

Copy link
Copy Markdown
Contributor Author

@irzhywau thank you, this changed the decision. We are not adopting Namespace for now: this PR will be closed without merging, and the trial result plus your findings go into #114, which closes as not adopted. elastos-runtime's access for the Namespace app gets removed, so a fork cannot route jobs there (your F1). The one good part of this PR, publishing only on a tag push, moves into a small separate PR together with cache-free tag builds, and I'll ask you to review that one.

@andersalm

Copy link
Copy Markdown
Contributor Author

Closing without merge under Anders’s decision: Namespace is not adopted.
The useful publication guard and cache-free tag builds are preserved in #138 (Part of #137), with @irzhywau requested to review.
Irzhy’s review and the adoption decision are recorded in #114.
The branch is retained. App access removal requires an organization owner; the exact settings are in #114 (comment).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants