Attested remote cache

Keyless-sign remote cache artifacts with Sigstore and verify the signer identity on download — provenance on top of the HMAC integrity layer.

Attested remote cache

The remote cache has two independent trust layers:

  • remoteCache.signing (HMAC)integrity. A shared secret proves a stored artifact was not tampered with. Anyone holding the secret can produce a valid MAC, so it answers "was this changed?" — not "who produced it?".
  • remoteCache.attestation (Sigstore)authenticity. Each artifact is keyless-signed in CI against a short-lived Fulcio certificate minted from the workflow's OIDC token and logged in Rekor. On download vis verifies the bundle and pins the signer identity, so a valid signature from an unexpected identity is rejected.

Use attestation when the cache is shared across trust boundaries (a public or multi-tenant bucket, untrusted PR runners) and you need "this artifact was produced by our release workflow," not just "this artifact is intact."

The payload signed and verified is the artifact's sha256, recomputed from disk on both ends — never the digest the server claims. A server that swaps bytes but echoes the requested hash still fails verification.

1. Install the optional dependency

Sigstore is an optional peer dependency — the weight (tuf-js, the bundle/protobuf machinery) is only paid where attestation is actually used:

pnpm add -D sigstore   # or: npm install -D sigstore / yarn add -D sigstore / bun add -d sigstore

If remoteCache.attestation is configured but sigstore is missing, vis run prints a startup warning with the exact install command and continues unsigned rather than failing the build. vis attest (interactive) offers to install it for you.

2. Configure the expected signer

expectedIdentity is required — verifying a bundle without pinning who signed it only re-proves integrity, which the HMAC layer already does. Pick one of three forms:

export default defineConfig({
    taskRunner: {
        remoteCache: {
            url: "https://cache.example.com",
            token: process.env.CACHE_TOKEN,
            attestation: {
                // (a) GitHub Actions preset — recommended
                expectedIdentity: {
                    github: {
                        repo: "visulima/visulima",
                        workflow: ".github/workflows/release.yml",
                        ref: "refs/heads/main",
                    },
                },
                requireOnDownload: true,
            },
        },
    },
});
FormWhen to useAnchoring
{ github: { repo, workflow, ref } }GitHub Actions (the common case)Expands to issuer https://token.actions.githubusercontent.com and the literal SAN https://github.com/{repo}/{workflow}@{ref}, regex-escaped and ^…$-anchored for you
{ oidcIssuer, san }Any other OIDC provider; you know the exact identity URIvis regex-escapes and anchors it — pass the plain URI, not a regex
{ oidcIssuer, sanRegex }Advanced: you genuinely need a pattern (e.g. any tag)Raw, unescaped regex — you own anchoring. Unanchored values are substring-matched

Why anchoring matters. sigstore-js matches the SAN with String.prototype.match, i.e. as an unanchored regex. An unanchored pattern is substring-matched, so a longer attacker-controlled SAN that merely contains your expected value would pass. The github and literal san forms are escaped and anchored automatically; only sanRegex exposes this, and vis run warns if a sanRegex is not ^…$-anchored.

3. Signing in CI

Signing needs an ambient OIDC token. In GitHub Actions, grant the job id-token: write:

permissions:
    contents: read
    id-token: write # required for keyless signing

vis detects ambient OIDC via CI=true, ACTIONS_ID_TOKEN_REQUEST_URL, or SIGSTORE_ID_TOKEN. With a token present, every cache upload is signed and the bundle travels in the X-Artifact-Attestation header. Outside CI (local dev) signing is skipped and the upload proceeds unsigned — local machines shouldn't block on a Fulcio round-trip they can't complete.

4. requireOnDownload and the mixed-producer caveat

requireOnDownload (default false) controls what happens to a download whose attestation is missing or fails verification:

  • false — treat it as a cache miss and re-execute the task. Soft; no provenance guarantee, but no hard failure.
  • true — reject it. Provenance is enforced.

Because local producers upload unsigned, a workspace that mixes local and CI producers against one cache will see local entries rejected by any consumer with requireOnDownload: true, forcing re-execution until CI repopulates them. That is intended. Set requireOnDownload: true only where every producer runs in CI (e.g. a read-only cache for PR runners, populated solely by the release workflow).

5. Observability

Two callbacks are wired to vis run's logger:

  • Rejectionremote cache entry <hash> rejected: attestation <invalid|missing>. Treating as a cache miss.
  • Verification failureattestation verification failed: <message>. The message is sigstore's own, so an unexpected-signer mismatch (certificate identity error - expected …, got …) is distinguishable from a broken chain or an expired-cert error at a glance.

Troubleshooting

SymptomCauseFix
…requires a pinned keyless signer via expectedIdentity at startupattestation block has no valid expectedIdentityAdd one of the three identity forms
Startup warning: sigstore package is not installedOptional dep missingRun the printed install command; uploads stay unsigned until then
verification failed: certificate identity error - expected X, got YSigner identity ≠ pinned identityThe artifact was produced by a different workflow/ref. Confirm repo/workflow/ref (or fix the producing workflow)
All CI hits re-execute under requireOnDownload: trueA local/unsigned producer populated the cacheEnsure every producer for that cache runs in CI, or set requireOnDownload: false

See also: Security audit for inbound provenance verification, and vis attest for standalone (non-cache) artifact attestation.

Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues