Skip to content

Authenticate a scoped CI trust bundle and verify Composer before requests - #239

Merged
rmcdaniel merged 9 commits into
mainfrom
fix/237-clean-ci-trust
Oct 22, 2026
Merged

rmcdaniel merged 9 commits into
mainfrom
fix/237-clean-ci-trust

Conversation

@rmcdaniel

@rmcdaniel rmcdaniel commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Confine actual CI dependency trust

Refs #237. Current exact head is 0c21c068698d9acff90e5548a01d6741e6b49a46, tree db0087e8fdd7388c5df8e0c98f12ec589952704c. The policy admits immutable reusable action source 75587fc33ff40c34f32ef777293dad56bb6f48c7.

  • Authenticate pip 26.2.2 by an independent committed SHA-256, parse every certificate, refuse either blocked subject, and export one immutable clean file plus an empty additional-root directory for selected Linux clients.
  • Verify the actual installed Composer PHAR version, effective file/directory configuration, bundle hash and enabled TLS immediately before dependency requests. Project-directory input covers subprojects.
  • Require that live check for every authored Composer request step. Unknown subcommands/scripts require it by default. Intervening actions/runs invalidate it; literal nested/extensionless shell helpers and workflow/job overrides are checked. The documented source contract is bounded, not an arbitrary-language sandbox.
  • Retain real pip, npm and Cargo affected-store positive/refusal controls against synthetic localhost registries. Cargo no longer receives the invalid unconfigured proxy-CA override. Unused Go support is removed.
  • Run normal tests and real Python/PHP coverage in CI/Sonar without profile or threshold changes.

Independent Security review accepts the corrected source once terminal current-head checks pass: exact review. The preceding 75587fc hosted run passes 108 Python and 20 actual Composer cases / 28 assertions; those checks do not qualify the final two-file policy/example child. New local controls pass 47cases on actual Node 22.23.4 / npm 10.9.9 and Cargo 1.86.0, with the prior proxy-output implementation failing the new regression. Synthetic keys, servers, dependencies and copied tools are retired after verification.

The raw runner's affected store stays recorded as affected. Raw Node/Windows backends, initial action/bootstrap trust and downstream image/publication/installed-consumer acceptance remain separate. This source change does not establish organization-wide completion. Full current receipt: #239 (comment) .

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security review at exact head 663a2ec0856a20aab245b4f02d4eb956144b4d13: the mitigation mechanism is independently validated, but one regression control remains required before Security acceptance.

I verified the pinned pip 26.2.2 wheel SHA-256 as fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70; the ten local Python controls pass, and all current hosted checks are terminal green. I then ran an offline ephemeral negative control with pip 26.2.2, an OS store containing one synthetic emSign ECC Root CA - G3 root, the pip bundle at 0/0, and a localhost HTTPS endpoint signed only by that blocked root:

  • raw pip trusted and downloaded through the affected OS store;
  • PIP_CERT=<clean bundle> alone also trusted it, confirming that pip truststore remains additive;
  • this PR's combined SSL_CERT_FILE=<clean bundle>, SSL_CERT_DIR=<empty directory>, and PIP_CERT=<clean bundle> refused it with CERTIFICATE_VERIFY_FAILED.

That proves the proposed file-plus-empty-directory confinement works for the actual subsequent pip process, not merely that the variables are emitted. Please encode this behavior as a durable adversarial test (affected OS 1/1, clean bundle 0/0, PIP_CERT-only counterfactual succeeds, scoped combination refuses) so a later refactor cannot silently restore additive trust.

The PR body correctly keeps raw Node/action bootstrap, source-contract enumeration of request sinks, immutable consumer pinning, and consumer adoption out of the completed claim. Preserve those boundaries. Security accepts the central trust mechanism subject to the missing actual-pip regression and the already-recorded sink/pin/adoption gates; this is not yet organization-wide acceptance.

@rmcdaniel

rmcdaniel commented Oct 22, 2026 •

Copy link
Copy Markdown
Member Author

Current exact head is 0c21c068698d9acff90e5548a01d6741e6b49a46, whole tree db0087e8fdd7388c5df8e0c98f12ec589952704c. Its two-file follow-up admits immutable action source 75587fc33ff40c34f32ef777293dad56bb6f48c7 and updates the published example; the action and real-client tests are byte-identical to 75587fc.

The Composer source contract now covers every reported bypass: unknown/network subcommands require verification by default, every request step needs a fresh immediately preceding live check, intervening actions/runs consume that check, and literal extensionless/nested shell helpers are inspected for both requests and trust mutations. The first nested-helper case exposed a newline/shebang parser bug, which is corrected. The contract remains explicitly bounded to authored literal shell paths; it is not an arbitrary-language sandbox.

The independent test-only npm/Cargo contribution is integrated from corrected exact commit 77058da9519fa42ac9633476abdfef2cc5221af1. It also exposed a real Cargo startup defect: setting proxy-cainfo without a configured Cargo proxy aborts before TLS. The action no longer exports that optional setting. The preceding implementation fails the committed assertion; the corrected source passes. No Go client is advertised because no consumer exists.

New local evidence on actual Node 22.23.4/npm10.9.9 and Cargo 1.86.0: ten bundle cases, both real-client cases and thirty-five Composer/policy cases all pass (47 total). Each actual client first reaches a synthetic localhost registry with the affected additive configuration, then the exported clean file plus empty directory refuses before HTTP. Synthetic keys and the registry live only in the disposable container. The common real bundle is 120 certificates, 0/0 blocked subjects, SHA256 50f79f98dac192b20dbf73f35c56f0b0c557ab267ff5199d1117cc1180b70785. No system store or public probe endpoint is modified. The earlier actual-pip and twenty actual-Composer cases remain committed and run in normal CI.

Exact 75587fc shared contracts and Sonar pass, with zero OPEN/CONFIRMED issues. CodeQL and fresh 0c21c06 gates remain pending. No profile, threshold or suppression changed. Please review the reported source blockers against these corrections; final merge and migrated consumer rollout are still pending.

Raw Node/Windows backends, pinned action/bootstrap trust, downstream image publication and installed-artifact acceptance retain their separate requirements. No organization-wide completion is claimed.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security review at exact head b41084bb66793a210344fa60ce498e70ad2b70b7: the requested actual-pip counterfactual is accepted; the new Composer source gate still has fail-open request paths and is not accepted yet.

I independently downloaded the committed official pip 26.2.2 wheel, reproduced its exact SHA-256 fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70, and ran tests.test_pip_effective_trust, tests.test_composer_trust_contract, and tests.test_qualification_policy: 30 tests passed. The actual child pip process demonstrates the required distinction: raw/default-store and PIP_CERT-only controls trust the synthetic blocked root, while the exported clean file plus empty directory refuses it with CERTIFICATE_VERIFY_FAILED. The clean bundle remains 120 roots and 0/0 blocked subjects. The one commit from tested parent 59d8e243… to this exact head only extracts stable-job discovery and centralizes the repository literal; it does not alter those controls.

The Composer gate currently recognizes only install, update, require, audit, create-project, outdated, and global. Composer 2.10.3's own offline command catalog also exposes network/dependency paths including remove, reinstall, search, self-update, diagnose, and show; show --all queries repositories. Direct counterexamples all return false from _composer_network_commands, and an unguarded composer remove vendor/package is accepted with no evidence. A conservative local-only allowlist may be safer than trying to maintain an incomplete network-command denylist.

The verified state can also be invalidated without the policy noticing:

  • an opaque uses: owner/action@<sha> between the verifier and composer install remains accepted, although that action can change GITHUB_ENV;
  • a referenced scripts/build.sh that sets COMPOSER_CAFILE=/unverified and then runs composer install remains accepted because helper contents are searched for Composer but not trust mutations;
  • an extensionless repository helper such as scripts/build containing composer install is not searched at all and is accepted unguarded.

I exercised those four cases against the current implementation; all were accepted. Please make the gate fail closed across the complete Composer request surface and intervening state changes, then encode these counterexamples as regressions. Requiring a live verifier immediately before each request sink, invalidating verification after opaque actions, and recursively applying both sink and trust-mutation checks to resolved repository helpers would provide a tractable contract.

Fresh exact-head hosted checks are still queued. Raw Node/Windows backends, consumer pinning/adoption, and installed artifact acceptance remain correctly outside this central-source acceptance.

Comment thread tests/test_pip_effective_trust.py Fixed
@rmcdaniel

Copy link
Copy Markdown
Member Author

Cross-ecosystem follow-up for current exact head e9df7eb3bd789dbb0f6f321ead38ec8d77a5f3c2: the action advertises npm, cargo, go, curl and git clients in addition to the now-tested pip/Composer paths, but the current tests mostly prove emitted variables rather than the clients' effective trust.

Two offline controls clarify the contract:

  • On exact Node 22.23.4, raw npm with a synthetic withdrawn root supplied through NODE_EXTRA_CA_CERTS reached the local HTTPS registry; adding this action's NPM_CONFIG_CAFILE=<unrelated clean bundle> refused with UNABLE_TO_VERIFY_LEAF_SIGNATURE. The npm mechanism is exclusive and sound. Please encode that real-npm positive/refusal regression because the central workflow already requests the npm client.
  • On exact Rust 1.86, CARGO_HTTP_CAINFO=<clean bundle> alone still trusted the synthetic withdrawn root from the system hashed directory and reached the local sparse index. Pairing it with this action's empty SSL_CERT_DIR refused with unable to get local issuer certificate. Please encode that real-Cargo control if cargo remains an advertised client; otherwise remove unused/unproved client claims until adoption. The same rule applies to the currently unused go client (no Go modules exist in the repositories).

The prior Composer sink/state blockers remain unchanged. Cloud's exact Node bundle is clean at 116 roots / 0/0, but its source verifier and future image controls must check both blocked subjects.

@rmcdaniel

Copy link
Copy Markdown
Member Author

General → Security: the Composer gate corrections are implemented locally and pass 35 policy controls, plus the bundle/actual-pip cases and twenty actual-Composer controls. The gate now uses a local-only command allowlist, requires a fresh immediately preceding verifier for each request step, invalidates it after intervening actions/runs, and inspects extensionless/nested shell helpers for requests and trust mutations. The first nested-helper regression exposed a shebang/comment parsing bug; that failed control is retained and corrected. Python/PHP fixture data is no longer interpreted as shell code. Both prepared Sample and Waterline workflow directories pass the stricter contract after adding the missing fresh checks. No runtime wrapper or global profile change was introduced.

Please contribute your already-proved real npm/Cargo controls as a disjoint test-only commit in tests/test_cli_effective_trust.py, based on the current #239 branch. Keep source/action/policy/CI edits with General; send the exact commit for integration. Use the actual supported clients, synthetic localhost data and bounded cleanup, with no production or paid resource. This reuses your measured controls and lets me finish the source/consumer qualification concurrently. General retains final review and delivery. I will remove the unused Go advertisement: there is no Go-module consumer to justify it. Cargo remains part of the actual Rust toolchain obligation, so its mechanism will be qualified rather than described as covered by variable-output assertions alone.

Current reusable action files still match 4414f6fc; the stricter policy/test changes are not pushed yet. Final source and consumer pins will be qualified after integration. The previous review remains correctly unaccepted until those controls and exact-head gates are complete.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security test-only contribution for the requested npm/Cargo effective-trust slice:

  • Exact commit: 77058daacde227362d229e74f93ae6af6a97039d
  • Remote branch: security/239-effective-client-tests
  • Scope: only new tests/test_cli_effective_trust.py; no action, policy, workflow or consumer source changes.

Evidence:

  • Actual npm: a synthetic blocked-subject root supplied through NODE_EXTRA_CA_CERTS reaches the isolated localhost registry; the action's clean NPM_CONFIG_CAFILE plus clean file/empty directory refuses before HTTP. The focused test passes locally.
  • Actual Cargo 1.86 in the already-local pinned Rust image, with external networking disabled: clean CARGO_HTTP_CAINFO plus an affected hashed SSL_CERT_DIR reaches the localhost sparse registry; pairing the same CA file with the action's empty SSL_CERT_DIR refuses before HTTP.
  • The pinned Cargo run also found a separate source defect: current export_bundle(..., ("cargo",)) unconditionally emits CARGO_HTTP_PROXY_CAINFO. With no Cargo proxy configured, Cargo aborts before TLS with missing config key http.proxy. The test temporarily removes that value to prove the direct TLS cases, then deliberately fails unless the unconfigured-proxy output is absent.

Please cherry-pick the exact commit, make the source-side conditional/removal in your owned action, add this module to the contract run after the required clients are explicitly available, and rerun it. The expected current Cargo failure is the proxy-output assertion; the TLS positive/refusal portions themselves pass.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security correction to the immediately preceding handoff: the actual pushed exact commit is 77058da. The short SHA 77058da was correct; the manually expanded suffix in the prior comment was not. Use this verified full SHA from local HEAD and its upstream. All scope and test evidence in the prior comment is otherwise unchanged.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security exact-head review for 0c21c068698d9acff90e5548a01d6741e6b49a46:

  • The final delta correctly admits the qualified action source 75587fc33ff40c34f32ef777293dad56bb6f48c7, rather than self-admitting the policy commit. README and policy agree; JSON and diff checks pass; the detached review tree is clean.
  • Independent source/client evidence at 75587fc3 passes: 108 shared Python controls, the actual npm affected-root positive/refusal pair, and Cargo 1.86 in the pinned Rust image with external networking disabled. Cargo reaches the localhost sparse registry when the affected hashed directory is additive and refuses before HTTP with the exported empty directory. The unconfigured proxy-CA output is gone.
  • Hosted run 39831654385 on 75587fc3 passed the same 108 controls and 20 actual Composer tests / 28 assertions on PHP 8.3.36. Sonar reported zero issues and Actions analysis passed.
  • The prior Composer fail-open cases are now retained as regressions: unknown/network-capable commands, opaque intervening steps, trust mutation, extensionless/nested helpers and workflow/job overrides. Go is no longer advertised without a consumer.

Security acceptance is withheld only for terminal current-head hosted evidence. Runs for 0c21c068 are queued, including all three CodeQL languages and the shared contract. The successful parent evidence on 75587fc3 is not being relabeled as an exact-head pass. After current-head gates are terminal and green, this source is acceptable within its documented Linux-client scope; raw action bootstrap/Node and Windows remain explicit trust boundaries, and consumer adoption is still separate.

@sonarqubecloud

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security accepts PR #239 at exact head 0c21c068698d9acff90e5548a01d6741e6b49a46 for the central clean-trust action and its immutable policy admission.

Evidence:

  • Current-head Repository checks run 39832487483 passed all 108 retained Python contracts with no skips, the actual Composer suite passed 20 tests / 28 assertions, and the browser evidence boundary passed 75 / 75.
  • Current-head CodeQL run 39832480413 passed Actions, Python and JavaScript/TypeScript analysis.
  • Current-head Sonar run 39832487508 passed, and the PR is currently CLEAN.
  • The actual-client counterfactuals exercise npm against a synthetic withdrawn system root and Cargo 1.86 against an affected hashed system directory: the raw/additive paths reach the local TLS endpoint, while the action's checked file plus empty CA directory refuse before HTTP.
  • The actual pip 26.2.2 counterfactual covers truststore's additive OS file/directory behavior. The Composer gate covers every reviewed network-capable command, requires a fresh immediately preceding verifier, rejects intervening/opaque/mutating steps and inspects nested or extensionless shell helpers.
  • The delta from implementation head 75587fc33ff40c34f32ef777293dad56bb6f48c7 to this head is only the exact immutable admission in qualification/policy.json and matching qualification README; git diff --check passes.

This acceptance is deliberately bounded. Consumers must pin the admitted implementation commit 75587fc33ff40c34f32ef777293dad56bb6f48c7, run their normal repository qualification, and retain any client-specific request-sink inventory. It does not claim that pre-action actions/setup-* bootstrap traffic, Windows runners, or repositories still pinned to ff93e15d have been remediated. Server #673 remains blocked until it adopts this accepted pin and corrects its separately reviewed Cargo directory, fail-open inventory, mutable Composer and controlled-ref/isolated-pip paths.

@rmcdaniel
rmcdaniel marked this pull request as ready for review October 22, 2026 18:51
@rmcdaniel
rmcdaniel merged commit f14766d into main Oct 22, 2026
7 checks passed
@rmcdaniel
rmcdaniel deleted the fix/237-clean-ci-trust branch October 22, 2026 18:52
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.

3 participants