How to troubleshoot a failed run¶
Diagnose why an attest-vm-image step failed and get to the fix. Classify the
failure first, then jump to the matching symptom.
Prerequisites¶
- The failed workflow run's logs. The action logs every command it runs.
- If your workflow uploads the evidence directory (default
./evidence) as an artifact, download it. Whether it exists and how complete it is tells you which kind of failure occurred.
Classify the failure first¶
A run fails in one of two ways. Determine which before doing anything else.
-
Look at the evidence directory (default
./evidence, or youroutput-directory). -
Empty, absent, or partial — missing
checksums.txtor one of the report/predicate files. This is a fail-closed abort: a stage threw before the evidence was complete. Continue at Fail-closed aborts. - Complete handoff —
checksums.txt, the SBOM,vulnerability-report.json,validation-report.json,validation-predicate.json, andevidence-manifest.jsonare all present. Usually this is an evidence-complete failure: the image was inspected and failed validation. Continue at Evidence-complete failures. - Unsigned set without
evidence-manifest.json— this is a late fail-closed abort, not an evidence-complete failure. Ifsigneris notnoneand the message names signing or attestations, the run passed validation and then aborted while signing; see signing failed but the evidence looks complete. Otherwise the manifest itself could not be completed, for example because an evidence file disappeared before it could be hashed.
The exact file list is in Evidence files.
- Confirm with the step outputs. They are the reliable discriminator and
settle the ambiguous case above. A thrown error sets no output at all: on any
fail-closed abort — including a signing failure —
disk-digest,checksums-path, and every other output are unset, even when evidence files already exist on disk. A pass and an evidence-complete failure both set the seven standard outputs, so their presence tells you the pipeline finished but not whether it passed. In short, the pre-manifest unsigned set with all outputs unset is a late fail-closed abort, not an evidence-complete failure. See Outputs.
Evidence-complete failures¶
The pipeline finished, wrote every evidence file, and then failed. The image did not pass validation.
Read the step's failure message. It names the reason(s): a vulnerability threshold breach, one or more failed contamination checks, or both. Its exact shape is the evidence-complete failure entry in Failure modes.
- Vulnerability threshold breach — findings met or exceeded your
fail-on-severity. Fix the image, or change what fails the run per Control what fails validation. - Failed contamination check(s) — the message lists the failing rule IDs. Remove the flagged artifact from the image, or adjust your policy per Control what fails validation.
The evidence is complete; keep it for audit even though the run failed.
Fail-closed aborts¶
A stage threw and the run stopped. Find your exact error string in Failure modes — it maps every message to a cause and a remedy. The sections below cover the aborts that are about the runner or its environment rather than the image itself.
Tool installation and the syft/grype downloads run before the disk is
validated, so apt-get and download log lines can appear even on a run that
ultimately aborts on a bad disk-path or a corrupt image. Read to the end of
the log for the thrown message.
Problem: the runner cannot install packages or use sudo¶
The action self-installs qemu-utils and libguestfs-tools with sudo apt-get
on every run.
Solution: Use a Linux runner that permits apt-get and passwordless sudo.
GitHub-hosted ubuntu-* runners qualify by default; self-hosted runners must
grant both. See Requirements.
Problem: no readable kernel image (/boot/vmlinuz-*)¶
The guest-inspection tooling needs a world-readable host kernel, and none
remained after the action's own chmod.
Solution: Use a runner whose /boot/vmlinuz-* can be made world-readable.
GitHub-hosted ubuntu-* runners work; some hardened self-hosted images strip or
lock down the kernel image. See Requirements.
Problem: the run aborts naming the platform or architecture¶
The action runs only on Linux x64 and arm64.
Solution: Move the job to an ubuntu-* runner (runs-on: ubuntu-24.04).
macOS, Windows, and other architectures fail closed; see the platform entries in
Failure modes.
Problem: a step before the action cannot build or mount the QCOW2¶
A step earlier in the same job that builds or mounts the image with
libguestfs/qemu — for example a guestfish seeding step — runs before the
action installs anything, so libguestfs-tools/qemu-utils are missing and
/boot/vmlinuz-* is not yet readable.
Solution: Install the tools and fix kernel readability in that step yourself, before the action runs:
- run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends qemu-utils libguestfs-tools
sudo chmod +r /boot/vmlinuz-*
The action's own install still runs later and is harmless. See the same-job note in Requirements.
Problem: runs are correct but extremely slow¶
On GitHub-hosted arm64 runners the guest-inspection tooling falls back to
software emulation.
Solution: Use an x64 runner (runs-on: ubuntu-24.04) for supported, fast
runs. arm64 is best-effort; see how-it-works.md for why it
is slow.
Problem: tool or database downloads fail¶
The action needs outbound access to download the pinned syft and grype
binaries, plus cosign when signer: sigstore-keyless, cosign-key, or kms
is selected, from github.com release assets and to fetch the Grype
vulnerability database at scan time. Keyless signing also needs GitHub OIDC and
the public Sigstore services. Symptoms are an integrity or download error while
fetching a binary, a Grype scan error, or a signing-service failure — see the
corresponding entries in Failure modes.
Solution: Allow egress to the endpoints listed under
Requirements. On locked-down or air-gapped runners,
pre-seed the Grype database and point Grype at it with the GRYPE_DB_CACHE_DIR
environment variable — set as a step- or job-level env: value, not an action
input — per Requirements.
Problem: signer: github fails on plan or visibility¶
The GitHub attestation API rejected the push. Two different messages indicate the same restriction:
- The translated plan/visibility diagnostic, which names the required repository type and permissions.
- The raw, untranslated API message
Failed to persist attestation: ... Feature not available ... upgrade the billing plan .... This billing-plan wording is not rewritten, but it means the same thing.
Both are cataloged in Failure modes.
Solution: Use a public repository, or a private/internal repository on GitHub Enterprise Cloud (GitHub Enterprise Server is unsupported), and grant the signing permissions. Full setup is in Publish signed attestations. The action never downgrades to unsigned output.
Problem: signer: github fails on a fork pull request¶
A pull request from a fork gets a read-only token and no OIDC identity, so it cannot sign.
Solution: Run signing on pushes or on same-repository pull requests only. See Publish signed attestations.
Problem: signer: sigstore-keyless names missing id-token: write¶
The action could not find GitHub's OIDC request URL and token. This happens when
the job omits id-token: write or runs for a fork pull request. The check runs
before tool download or disk access, so the named permission error is expected
even when disk-path would fail later.
Solution: Grant contents: read and id-token: write, and run keyless
signing only on a trusted same-repository event. Do not grant
attestations: write; keyless signing does not use the GitHub attestation API.
See Sign with public Sigstore keyless identity.
Problem: signer: kms cannot fetch or use the key¶
KMS input validation checks the provider and immutable/versioned locator shape, but provider authentication and key capabilities are checked by the live API. A redacted Cosign command failure usually means the preceding authentication step did not establish ambient credentials, the principal lacks public-key or signing permission, or the selected key cannot sign.
Solution: Check the provider authentication step and audit log. Confirm the principal is limited to the exact key but can fetch its public key and sign, and confirm the key is asymmetric and signing-capable. Vault/OpenBao additionally require their named address and token environment variables. See Sign with a KMS or Transit key.
Problem: signing failed but the evidence looks complete¶
A signing failure aborts after the unsigned evidence is sealed but before the
handoff is written. The checksum manifest and unsigned evidence documents exist
on disk, but evidence-manifest.json does not and — as with every thrown error
— the run sets no outputs. attestation-url, attestation-bundle-path, and the
seven standard outputs are all empty.
Solution: Fix the signing failure using the relevant entry above, then re-run. The unsigned evidence from the failed run is valid on its own; a later step that reads outputs must tolerate them being unset.
Related¶
- Reference: Failure modes — every error message, its cause, and its remedy.
- Reference: Requirements — runner, permission, and network prerequisites.
- Control what fails validation — threshold and policy tuning.
- Publish signed attestations — signing permissions and setup.
- How attest-vm-image works — the failure model and why arm64 is slow.