IMPORTANT: Developer documentation for the current development branch. This content is unreleased, may change without notice, and must not be treated as Buildish release documentation.
Security
GitHub Actions permissions
Ordinary standalone and same-run distributed workflows need only the permissions required by their
other steps. The examples use contents: read for actions/checkout; cache restore/save and
same-run delta artifact upload/list/download/delete use GitHub-provided, job-scoped Actions runtime
credentials rather than the workflow’s GITHUB_TOKEN. Do not grant actions: write for those
operations—it is broader than this action needs.
Explicit cross-run or cross-repository artifact lookup is a separate API boundary and is not used by
the current ordinary workflow contract. Such a feature requires a token with access to the target
run/repository and must document its actions permission at that point. Likewise, grant additional
permissions only when another workflow step or an authenticated GitHub API request requires them.
Read-only workers upload no envelopes. Read-only aggregators do not contact the artifact backend at
all: their dependent-delta result is skipped-read-only. When possible, skip the aggregator job on
pull-request events to avoid allocating a runner that can only perform this no-op.
The github-token input (or GITHUB_TOKEN environment variable) is used only by the Gradle
action for authenticated wrapper JAR downloads against the GitHub API. The Maven action does not
use it. It is never written to job summaries or persisted in post-action state.
Security
Gradle wrapper verification
Every gradle-wrapper.jar provisioned by this action goes through a three-step verification chain
before it is written to disk:
- SHA-256 checksum — the expected digest is downloaded from
services.gradle.orgover HTTPS and compared to the downloaded JAR bytes. - Detached OpenPGP signature — the ASCII-armored
.ascsignature is downloaded fromservices.gradle.organd verified against a pinned Gradle signing-key allowlist using a fresh ephemeral GnuPG home. The pinned keys live insrc/build-tool/gradle/wrapper/signature.ts. - Race-condition guard — the JAR is written atomically via a temporary file so a partially written JAR is never exposed to the Gradle invocation.
The key allowlist is designed to support smooth Gradle signing-key rotation: old and new keys may overlap in the allowlist. Remove a retired key only after it no longer signs any wrapper version you intend to support.
To override the GnuPG binary (for example, on Windows runners where multiple gpg variants may
exist), set the BUILDISH_MAMMOTH_CACHE_GRADLE_GPG_COMMAND environment variable before the action
runs.
Token scoping
github-tokenis used exclusively by the Gradle action for authenticated requests toapi.github.comandraw.githubusercontent.comwhen downloading wrapper JARs. It is applied per-host so it is never sent to any other endpoint.- The token is never written to workflow summaries, log output, or post-action state.
Threat model
Assumed attacker capabilities
The threat model considers two attacker categories:
- External attacker — no repository access, no ability to push commits or create pull requests. An external attacker cannot interact with the repository’s GitHub Actions cache at all.
- Contributor with pull-request access — can open a pull request and trigger
pull_request/pull_request_targetworkflow runs. This is the primary threat actor for cache-poisoning scenarios.
Actors with direct push access to a repository’s default branch are considered trusted. The security posture of CI in general degrades when untrusted actors gain push access, which is outside the scope of this threat model.
Impact of a successful cache poisoning
GitHub Actions cache entries are scoped to a branch and a workflow. A poisoned cache entry means that malicious files — for example, a trojaned build-tool JAR or a plugin that executes arbitrary code — can be injected into a future build that restores from that entry. Because the build tool (Gradle or Maven) loads and executes JARs from its local cache directory, arbitrary code execution within the build is a realistic impact of a successfully poisoned entry.
The most dangerous scenario is poisoning the default branch (main / master) cache,
because all PR builds fall back to it via the restore key chain.
Mitigations already in place
| Mitigation | Detail |
|---|---|
| Read-only by default on PRs | pull_request and pull_request_target events default to read-only: true. Worker and aggregator jobs on PRs do not save cache entries or upload delta artifacts to the shared backend, so a PR build cannot mutate the default-branch cache. |
| Trusted lowering of the PR floor | Repository config can enable read-only mode but cannot disable the pull-request default. Only a direct workflow input can make that trusted-workflow decision. |
| Gradle wrapper JAR verification | Every wrapper JAR is verified with a SHA-256 checksum and a GnuPG detached signature against a pinned key allowlist before it is written to disk. A tampered JAR downloaded from a compromised distributionUrl is rejected before Gradle is invoked. |
| Hard cache exclusions | Paths that could carry encryption key material, PID-bearing lock files, or per-runner absolute paths are excluded unconditionally and cannot be re-enabled via cache-partitions overrides. See Hard cache safety exclusions. |
| Token never persisted | github-token is applied per-host and never written to summaries, logs, or post-action state. A token leak via a poisoned cache is therefore not possible through this action. |
| Delta artifact integrity verification | Before a distributed aggregator applies any worker delta, it verifies each file’s SHA-256 against the metadata in the artifact’s delta-package.json. A corrupted or tampered artifact is rejected before any file is written to the cache directory. |
Residual risks
- Cache entries are not verified against their generation digest at restore time. GitHub’s
cache backend selects entries by key prefix and creation time. A malicious actor with cache-write
access can publish a newer generation in a compatible lineage, and the action does not rehash the
restored archive before use. This is a fundamental limitation of the underlying
@actions/cacheservice. - Third-party plugin JARs are not re-verified after the initial build. Once a plugin JAR is
cached, subsequent restores trust the cache entry. A poisoned entry that replaces a legitimate
plugin JAR will execute on the next build without further verification. Mitigate this by
using lockfiles, dependency verification (Gradle’s
--write-verification-metadata), orread-only: trueon jobs where cache freshness is not required. mainbranch cache affects all subsequent workflows. A single poisoned save to the default-branch cache lineage propagates to every future build on that branch until a clean generation becomes the newest entry. Treat cache write access with the same sensitivity as push access.
Hard cache safety exclusions
The following paths are excluded from every active partition unconditionally and cannot be overridden.
Gradle
| Pattern | Reason |
|---|---|
**/configuration-cache/** |
May contain encrypted secrets; volatile by nature |
**/*.lock |
PID-bearing files that cause hangs if restored on another runner |
caches/*/cc-keystore |
Configuration-cache encryption key material |
caches/journal-1/** |
Gradle’s local-only file-access journal; migrating it causes corruption |
Maven
| Pattern | Reason |
|---|---|
**/*.lastUpdated |
Stale remote-check markers; cause silent re-resolution when shared across runners |
**/resolver-status.properties |
Maven Resolver group-level remote-check status; per-runner state and a common distributed-merge conflict source |
**/_remote.repositories |
Records which remote a file came from; not portable across different CI environments |
**/*.lock |
PID-bearing resolver lock files that cause hangs if restored on another runner |