diff --git a/content/chainguard/actions/overview.md b/content/chainguard/actions/overview.md index f6e00c1b4b..dd3466a388 100644 --- a/content/chainguard/actions/overview.md +++ b/content/chainguard/actions/overview.md @@ -16,18 +16,77 @@ toc: true Chainguard Actions are a set of hardened drop-in replacements for popular GitHub Actions. Each action preserves the same inputs and outputs as the upstream version, but has been examined and revised to better protect your CI/CD pipelines from supply chain attacks. The only change in your workflow configuration is the name of the action in the `uses:` line. -Coverage spans GitHub first-party (`actions/*`), cloud-provider (`aws-actions/*`, `azure/*`, `google-github-actions/*`), Docker, HashiCorp, and security tools actions (Trivy, Grype, CodeQL, Semgrep), as well as a growing catalog of community actions. +The catalog holds more than 1,000 hardened actions. Coverage spans GitHub first-party (`actions/*`), cloud-provider (`aws-actions/*`, `azure/*`, `google-github-actions/*`), Docker, HashiCorp, and security tools actions (Trivy, Grype, CodeQL, Semgrep), as well as a growing catalog of community actions. Each hardened action: -- Is built from source and evaluated through a rule-based and AI-powered hardening pipeline +- Is pulled from the upstream source at a pinned commit, then reviewed by a static ruleset and an AI-powered analysis pass - Has every internal `uses:` and container image reference pinned to an immutable SHA digest - Ships with a `HARDENING.md` report documenting exactly what was checked and fixed +- Ships with a signed SLSA provenance attestation recording the upstream source and the ruleset version applied (releases published before signing began don't carry one) - Is re-reviewed and re-hardened whenever upstream publishes a new version or Chainguard adds a new rule Chainguard Actions protect against common threats including tag hijacking, dependency confusion, `pull_request_target` abuse, and secret exfiltration. -This page provides enough to get you started. Refer to the [Chainguard Actions README](https://github.com/chainguard-actions) in GitHub for deeper technical details and some example migrations. You can also [use Guardener to enable Chainguard Actions](/chainguard/guardener/github/actions-security/). +This page provides enough to get you started. Browse the [Chainguard Actions organization](https://github.com/chainguard-actions) to find a specific action and read its hardening report. + +## What hardening checks and fixes + +Every action in the catalog goes through the same two-stage review. A deterministic static pre-pass runs first and catches patterns mechanically. An AI-powered analysis pass then evaluates the action against the policy ruleset. Findings from either stage carry an ID that appears in the action's `HARDENING.md` report, so you can trace any change back to the check that produced it. + +### Policy checks + +| Check | Finding IDs | Severity | What it catches | +| ----- | ----------- | -------- | --------------- | +| Unpinned uses | `unpinned-uses` | High | A `uses:` reference or a `runs.image:` container reference pointing at a mutable tag or branch instead of an immutable commit SHA or image digest. The `docker://` prefix is optional, so `image: ghcr.io/example/tool:latest` is a finding too. | +| Script injection | `script-injection` | High | Expressions such as `${{ inputs.name }}` interpolated directly into a `run:` block, where the shell can parse attacker-controlled text as commands. | +| Unsafe shell | `unsafe-shell` | High | Remote content piped straight into an interpreter, such as `curl ... \| bash`. | +| Hardcoded credentials | `hardcoded-credentials` | High | Literal secrets assigned to names containing `password`, `secret`, `token`, `api_key`, or `aws_secret`. | +| GitHub environment injection | `github-env-injection` | High | Untrusted values written to `$GITHUB_ENV`, `$GITHUB_PATH`, or `$GITHUB_OUTPUT` without newline sanitization, which lets an attacker inject variables into later steps. | +| Suspicious run content | `suspicious-run-content` | High | Malicious patterns in `run:` blocks, including obfuscated execution, process memory access, dynamic evaluation, credential file access, outbound exfiltration, reverse shells, persistence, and environment secret scraping. | +| Permissions | `permissions`, `missing-permissions`, `broad-permissions` | Medium | Workflows that leave `GITHUB_TOKEN` at its default permissions, or that set `read-all` or `write-all` instead of specific scopes. | + +### Static pre-pass checks + +The static pre-pass complements the analysis pass by catching patterns the analysis may miss. It reports one finding per occurrence, so a report can list the same ID many times, each with its own location. `static-inline-injection` is the most common finding in the catalog for that reason. + +| Finding ID | Severity | What it catches | +| ---------- | -------- | --------------- | +| `static-inline-injection` | High | A single expression interpolated directly into a `run:` block. The finding names the expression and the step it appears in, and the fix moves the value into an `env:` map. | +| `static-unsanitized-env-write` | Medium | An unsanitized write to a GitHub environment file. | +| `invalid-yaml` | High | An action or workflow file that could not be parsed. | + +Both stages read the action definition (`action.yml` or `action.yaml`) and every workflow under `.github/workflows/` in the action's own repository. Neither inspects built or vendored output: `dist/`, `vendor/`, and `node_modules/` are out of scope, as are the action's test fixtures. + +Findings are fixed in place and the pipeline re-evaluates its own work, so a single hardening run can take several iterations before an action passes. Both the findings and the per-iteration notes are recorded in `HARDENING.md`. + +## Transitive dependencies + +Actions rarely run alone. A composite action can call other actions, and an action can fetch container images, language packages, or binaries while it runs. Chainguard hardens the references it can see in the action's source: + +- **Nested action and image references are pinned.** Every `uses:` reference and container image reference inside a hardened action resolves to an immutable commit SHA or image digest, with the original tag preserved as a comment. A moved upstream tag can't change what a hardened action runs. +- **The action's own workflows are reviewed too.** Both stages of the review cover the workflows under `.github/workflows/` in the action's repository, not only the action definition. +- **Missing dependencies can be onboarded.** When a hardened action depends on an action that isn't in the catalog yet, [request that action](https://github.com/chainguard-actions/.github/issues/new?template=new-action.yml) and Chainguard hardens and publishes it. + +### Rewriting nested references to hardened equivalents + +Pinning a nested reference to an upstream commit SHA freezes what runs, but the code it freezes is still the upstream project's. Chainguard is rolling out a dependency graph that replaces those references with the Chainguard hardened counterpart instead, also pinned by commit SHA. When a dependency is hardened and published, every action that depends on it returns to the hardening queue so its `uses:` reference can be rewritten. + +The graph is enabled in production and rewriting is rolling out across the catalog, so a given action may not have been rewritten yet. Three limits apply where it does: + +- It covers composite actions only. Node and Docker actions have no `uses:` steps to rewrite. +- It fires only when a hardened counterpart exists for that exact upstream commit. +- It never rewrites a reference when the correct counterpart is ambiguous. + +To see what a particular action references today, read its `action.yml` on the version branch you plan to use. + +### Dependencies an action installs when it runs + +Dependency vulnerability management is not part of hardening today. When Chainguard rebuilds a JavaScript action's bundle, the builder installs exactly what the upstream lockfile pins, so the hardened action ships the same dependency versions the upstream release shipped. The rebuild reproduces the bundle rather than refreshing it: if an upstream release bundled a vulnerable package, so does the hardened release. Language packages and binaries that an action downloads while it runs are likewise outside what the review inspects. + +Dependency handling is an area Chainguard is actively building out, and the nested-reference rewriting described earlier is the first piece of it. + +To see the full dependency graph for your own repository, including actions reached through other actions, use the `--recursive` flag described in [View the actions you are currently using](#view-the-actions-you-are-currently-using-in-a-repository). ## Prerequisites @@ -37,9 +96,11 @@ To follow this guide, you need: - An active Chainguard organization. - Owner access on the organization. -## Preliminary steps +## Set up Chainguard Actions + +Setting up has two parts: entitle your organization, then choose how you migrate your workflows. -Before using Chainguard Actions, log in to Chainguard and enable the Chainguard Actions entitlement for your organization. +### Step 1: Create the Actions entitlement Authenticate using `chainctl`: @@ -71,21 +132,75 @@ chainctl actions entitlements list $ENTITLEMENT_ID | 2026-06-18 17:33:24 UTC ``` +#### What the entitlement controls + +The entitlement records your organization's access to Chainguard Actions. It does not gate consumption of the actions themselves, and it can't: the hardened action repositories are public, and GitHub provides no mechanism to require authentication to consume a public action. + +Most hardened actions run a hook that records a usage event to `https://actions.enforce.dev/actions/v1/record`: a `pre` script in JavaScript actions, or the first step in composite actions. Docker actions don't include it. The hook returns no authorization decision, so there is nothing for the action to act on. It times out after 2 seconds and discards every error, which means Chainguard being slow or unreachable cannot fail your workflow. If your runners use an egress allowlist, add that host so the hook doesn't spend its timeout on every step. + +Refer to [Chainguard Actions telemetry and privacy](/chainguard/actions/telemetry/) for what the hook records and how to limit it. + +### Step 2: Install the Guardener GitHub App + +The [Guardener](/chainguard/guardener/github/getting-started/) GitHub App is the recommended way to adopt Chainguard Actions across more than a repository or two. Once you install it and link it to your Chainguard organization, Guardener: + +- Inventories the actions your workflows use across every repository it can access +- Comments on pull requests that introduce unhardened actions, so your workflows don't drift back +- Opens and maintains a pull request that swaps in Chainguard hardened equivalents, once you enable migration + +To set it up: + +1. Install the [Guardener GitHub App](https://github.com/apps/chainguard-guardener) on your GitHub organization. +2. Link your Chainguard organization to your GitHub organization with `chainctl guardener github link`. +3. Add a `.chainguard/actions.yaml` file to the root of each repository you want Guardener to work on. + +Both of the last two steps matter. Installing the app changes no repository on its own, and the Actions feature stays inert until `.chainguard/actions.yaml` exists in the repository. Once it does, pull request recommendations are on by default, but automated migration pull requests need `migrate.enabled: true` set explicitly: + +```yaml +enabled: true +migrate: + enabled: true +``` + +If installing an app in your organization needs an administrator's approval, they will be asked to approve a specific set of GitHub permissions. [Permissions Guardener requests](/chainguard/guardener/github/getting-started/#permissions-guardener-requests) lists each one and why it's needed, so you can take that to them before you start. + +Refer to [Getting started with Guardener](/chainguard/guardener/github/getting-started/) for the installation and linking steps, and to [Hardened Actions](/chainguard/guardener/github/actions-security/) for the configuration reference, the migration options, and the on-demand migration command. + +The Guardener GitHub App is in beta. It runs in production and is supported, but its features and configuration may still change. + +If you'd rather not install a GitHub App, you can migrate with the [cg-actions](https://github.com/chainguard-dev/cg-skills/tree/main/skills/cg-actions) skill or by hand. Both approaches are covered in [Configure your workflows to use Chainguard Actions](#configure-your-workflows-to-use-chainguard-actions). + ## Basic usage (quick start) To use a Chainguard hardened action, edit your workflow's YAML configuration file and change the `uses:` line to match the location in `chainguard-actions`: ```yaml -- uses: chainguard-actions/@main +- uses: chainguard-actions/@ ``` -Action names often have the upstream organization appended to the action name for clarity, for example, `tj-actions/changed-actions` becomes `tj-actions-changed-actions`. This prevents two different sources of a `changed-actions` action from clashing in the Chainguard Actions repository. +Repository names are prefixed with the upstream organization, so `tj-actions/changed-files` becomes `tj-actions-changed-files`. This keeps two different sources of a `changed-files` action from clashing in the Chainguard Actions organization. Search the Chainguard Actions repository, find the action you want to use, and then use the name you find there. -> **Note:** This example uses `@main`, a mutable reference, to illustrate the mechanics of switching organizations. For production workflows, pin to an immutable SHA digest instead. The [Configure your workflows](#configure-your-workflows-to-use-chainguard-actions) section covers the full migration. +> **Note:** Don't reference a hardened action with `@main`. The main branch of each repository holds only metadata (`README.md`, `LICENSE_CHAINGUARD`, and `source.json`). The hardened action itself lives on the version branches, so a reference to `@main` fails to resolve. -The rest of this page goes a bit deeper into how to use Chainguard Actions. +This example uses a version tag to show the mechanic, which is all that changes in your workflow. For any workflow you intend to keep, pin to a commit SHA instead, as described in [Choose how to reference an action](#choose-how-to-reference-an-action). + +## Choose how to reference an action + +Pin to a commit SHA, and pair the pin with Dependabot or Renovate: + +```yaml +- uses: chainguard-actions/actions-checkout@ # v4 +``` + +A commit SHA is the only immutable reference in the catalog. Tags are mutable by design, and not just the floating major version. Chainguard re-hardens published versions in place and moves the tag when it does, including fully qualified patch tags, so a single upstream release can be re-hardened several times with the same tag pointing somewhere new each time. Both `@v4` and `@v4.3.1` resolve to whatever was published most recently. + +Pinning on its own isn't enough, though. A pin with no tooling behind it is the one configuration that strands you: you stay on that build, and stop receiving re-hardening, until someone updates the SHA by hand. Dependabot and Renovate both track a pinned SHA against its tag and open a pull request when the tag moves, so you receive every re-hardening as a change your own CI validates before it reaches a live workflow. That gives you more control than a mutable tag does, and costs you nothing in freshness. + +Pinning is also the standard Chainguard applies to the actions it hardens. The `unpinned-uses` check fails any `uses:` reference on a tag, so if you run an actions linter against your own repository, referencing a hardened action by tag will register a finding. + +Use the canonical repository name in the reference. Some catalog repositories answer to an older name through a GitHub rename redirect — `chainguard-actions/checkout` reaches `chainguard-actions/actions-checkout`, for example — but a redirect isn't something to depend on in a pinned workflow. ## Configure your workflows to use Chainguard Actions @@ -101,6 +216,8 @@ Run this from the root of your repository to get a deduplicated list of every `u grep -rhE "uses:\s*[^@]+@" .github/workflows/ | sort -u ``` +For a more thorough inventory that also follows composite actions, use [`chainctl actions discover`](#view-the-actions-you-are-currently-using-in-a-repository). + ### Check the Chainguard Actions catalog for each action. Browse [the Chainguard Actions repository](https://github.com/chainguard-actions) or use the GitHub search UI. Match by organization and action name — for example, if you use `tj-actions/changed-files`, search for `org:chainguard-actions tj-actions-changed-files`. @@ -109,7 +226,7 @@ If the action isn't in the catalog, [open an issue](https://github.com/chainguar ### Replace the `uses:` line in each workflow. -Change the `uses:` line to match the location in `chainguard-actions`. Find and pin to the commit SHA digest and preserve the original tag as a comment so Dependabot, Renovate, and human reviewers can track upgrades: +Change the `uses:` line to match the location in `chainguard-actions`. To pin by SHA digest, preserve the original tag as a comment so Dependabot, Renovate, and human reviewers can track upgrades: ```yaml # Before @@ -129,7 +246,7 @@ gh api repos/chainguard-actions/tj-actions-changed-files/commits/v47 --jq '.sha' ``` ```output -25a1eb5aa40568ec6f8c0e58f2e809ef4270ebfa +4b4bd2ed96c7629e1c911f97f2390b91e1362735 ``` For the short SHA digest: @@ -139,15 +256,17 @@ gh api repos/chainguard-actions/tj-actions-changed-files/commits/v47 --jq '.sha[ ``` ```output -25a1eb5 +4b4bd2e ``` The resulting `uses:` line with the full SHA digest: ```yaml -- uses: chainguard-actions/changed-files@25a1eb5aa40568ec6f8c0e58f2e809ef4270ebfa # v47 +- uses: chainguard-actions/tj-actions-changed-files@4b4bd2ed96c7629e1c911f97f2390b91e1362735 # v47 ``` +Run the command rather than copying the digest shown here. Because Chainguard re-hardens a published version in place and moves its tag, the SHA a version tag resolves to changes each time that version is re-hardened. + ### Update your allowed-actions list. If your GitHub organization or repository restricts which actions can run (**Settings > Actions > General > Allow select actions**), add `chainguard-actions/*` to the allowed patterns. Without this, workflows fail with a policy error on first run. @@ -164,7 +283,7 @@ If something breaks, [file an issue](https://github.com/chainguard-actions/.gith ## View the actions you are currently using in a repository -Use `chainctl` to scan every workflow and composite action in a repository and list all dependencies transitively: +Use `chainctl` to scan every workflow and composite action in a repository and list the actions and container images they reference: ```shell chainctl actions discover $GIT_ORGANIZATION/$REPO @@ -181,6 +300,16 @@ chainctl actions discover $GIT_ORGANIZATION/$REPO ``` +The command needs a GitHub token, which it reads from `$GITHUB_TOKEN` or from `gh auth token`. The target can be a local directory (the current directory by default), an `owner/repo` pair, or a single action reference such as `actions/checkout@v4`. + +By default, `discover` lists only the actions your workflows reference directly. Add `--recursive` to follow each referenced action into its own definition and resolve the full transitive dependency graph: + +```shell +chainctl actions discover $GIT_ORGANIZATION/$REPO --recursive +``` + +A recursive scan makes many GitHub API calls, so it caches responses and stops after `--timeout` (five minutes by default). Refer to [`chainctl actions discover`](/platform/chainctl/chainctl-docs/chainctl_actions_discover/) for the full set of flags. + ## View the actions currently available While you can search the [Chainguard Actions repository](https://github.com/chainguard-actions) directly in GitHub, you can also use `chainctl` to find an action. @@ -197,36 +326,55 @@ chainctl actions catalog list --upstream-owner=tj-actions This example returns a list of all actions in the Chainguard Actions repository that originate from the `tj-actions` upstream source. -## Hardened action repository contents +To list the catalog entries available to a specific organization rather than the whole public catalog, use [`chainctl actions list`](/platform/chainctl/chainctl-docs/chainctl_actions_list/): + +```shell +chainctl actions list --parent $ORGANIZATION +``` + +## What ships in each hardened action -The main branch of each hardened action repository contains: +Each hardened action's repository has a main branch and one branch per hardened version. The hardened action lives on the version branches; the main branch holds only metadata. -- `HARDENING.md` — the authoritative, per-action record of what was checked, what was fixed, and how +The main branch of each repository contains: + +- `README.md` — a pointer to the action and its upstream source +- `LICENSE_CHAINGUARD` — the Chainguard license for the hardened variant +- `source.json` — a manifest naming the upstream owner, repository, version, and commit, along with the policy SHAs applied + +Each version branch contains: + +- `HARDENING.md` — the authoritative, per-action record of what was checked, what was fixed, and how, including the policy SHA that pins the exact ruleset applied - `action.yml` or `action.yaml` — the hardened action definition, preserving upstream inputs and outputs with fixes applied +- `attestations/provenance.intoto.jsonl` — a signed [SLSA provenance](https://slsa.dev/provenance/v1) attestation naming the upstream repository and commit, the ruleset version, the build times, and a SHA-256 digest for every file in the hardened action - `LICENSE_CHAINGUARD` — the Chainguard license for the hardened variant -- `source.json` and `published.json` — manifests pointing at the upstream source and the upstream version being tracked (not yet present in all repos; some older repos don't include them) -- Some actions also include documentation from upstream that you can adapt to use with the Chainguard hardened version +- The upstream action's own files, including its license and any documentation you can adapt for the hardened version -Then, the version branches in the hardened action repos contain the hardened actions. +Because `HARDENING.md` and the attestation are per-version, read them on the version branch you plan to use rather than on the main branch. + +Nearly every version branch in the catalog carries an attestation. A small number of older releases were published before Chainguard began signing them, so if a version branch has no `attestations/` directory, treat that release as unverifiable rather than as verified. + +Chainguard doesn't publish a customer-facing verification procedure yet. Verification requires the signing key's fingerprint, which isn't published, so there is no complete recipe to follow today. Tooling for this is planned. In the meantime the attestation is still useful as a record: it names the upstream commit the release was built from and the ruleset version that was applied. ## The continuous re-hardening process Chainguard Actions are continuously re-hardened: - When upstream publishes a new version, the pipeline re-runs and publishes a new hardened version -- When the hardening ruleset is updated, affected actions are re-reviewed against the new rules +- When the hardening ruleset is updated, every action in the catalog is re-evaluated against the new ruleset and re-hardened as needed - The `HARDENING.md` report is regenerated on every hardening run, with its own policy SHA pinning the exact set of rules that were applied -## Request a new action or report an issue +Because the policy SHA is computed over the ruleset itself, any change to a rule produces a new SHA, which is what triggers the catalog-wide re-evaluation. -To request a new action, [open an issue](https://github.com/chainguard-actions/.github/issues/new?template=new-action.yml). +## Request a new action or report an issue -## Report an issue +To request an action that isn't in the catalog, [open a new action issue](https://github.com/chainguard-actions/.github/issues/new?template=new-action.yml). -If an action isn't working as expected, [open an issue](https://github.com/chainguard-actions/.github/issues/new?template=action-issue.yml) with the action reference, a description of the problem, and steps to reproduce. +If an action isn't working as expected, [open an action issue](https://github.com/chainguard-actions/.github/issues/new?template=action-issue.yml) with the action reference, a description of the problem, and steps to reproduce. ## Learn more - [Chainguard Actions telemetry and privacy](/chainguard/actions/telemetry/) +- [Hardened Actions with Guardener](/chainguard/guardener/github/actions-security/) - [Chainguard Actions product page](https://www.chainguard.dev/actions) - For other questions, [contact Chainguard](https://www.chainguard.dev/contact?utm=docs). diff --git a/content/chainguard/actions/telemetry.md b/content/chainguard/actions/telemetry.md index 9c4d14742c..7079b9abe4 100644 --- a/content/chainguard/actions/telemetry.md +++ b/content/chainguard/actions/telemetry.md @@ -14,7 +14,9 @@ weight: 20 toc: true --- -Every Chainguard hardened action runs a best-effort "phone-home" pre-hook that records a usage event to `https://actions.enforce.dev/actions/v1/record`. The hook is fire-and-forget, with a 2 second timeout that fails open, so it cannot break your build. +Most Chainguard hardened actions run a best-effort "phone-home" hook that records a usage event to `https://actions.enforce.dev/actions/v1/record`. JavaScript actions run it as a `pre` script, and composite actions run it as their first step. Docker actions don't include the hook, and some JavaScript and composite actions don't either. The hook is fire-and-forget, with a 2 second timeout that fails open, so it cannot break your build. + +If your runners use an egress allowlist, add `actions.enforce.dev` so the hook doesn't wait out its timeout on every step that uses a hardened action. ## Why we collect this data @@ -28,7 +30,9 @@ We collect this data for two reasons: What we collect depends on whether your workflow grants `id-token: write`: - **Without `id-token: write`**: we record your repository name, a timestamp, and an "unverified" flag. -- **With `id-token: write`**: the hook mints a GitHub OIDC token scoped to the `actions.chainguard.dev` audience and sends it so we can verify the record. From that token we store metadata: repository, actor, ref, sha, workflow path, repository visibility, and run identifiers. +- **With `id-token: write`**: the hook mints a GitHub OIDC token scoped to the `actions.chainguard.dev` audience and sends it so we can verify the record. From that token we store metadata: repository, ref, sha, workflow path, repository visibility, and run identifiers. + +We do not store who triggered the run. The OIDC token identifies the account that started the workflow, and our service discards that claim where it assembles the usage event, so the actor never reaches any of our storage. The hook never grants itself `id-token: write`. It only uses the permission if your workflow already grants it. If you would rather we receive only your repository name, do not grant `id-token: write` to that job.