Skip to content

fix: keep egress-policy=block when the cache host can't be resolved (#675) - #676

Draft
JPHutchins wants to merge 1 commit into
step-security:mainfrom
JPHutchins:fix/675-egress-block-fail-closed
Draft

fix: keep egress-policy=block when the cache host can't be resolved (#675)#676
JPHutchins wants to merge 1 commit into
step-security:mainfrom
JPHutchins:fix/675-egress-block-fail-closed

Conversation

@JPHutchins

Copy link
Copy Markdown

What

Under egress-policy: block, harden-runner must never silently downgrade itself to audit. Today it does, whenever it can't resolve the Actions cache host during arming. This PR makes that path fail closed (keep block) instead of fail open.

Fixes #675.

Why

In src/setup.ts, block-mode arming fetches the harden-runner-cacheKey cache entry only to discover the cache blob host and add it to the allowlist:

const response = await twirpClient.GetCacheEntryDownloadURL(request);
const url = new URL(response.signedDownloadUrl);   // throws when the entry doesn't exist
...
} catch (e) {
  core.info(`Unable to fetch cacheURL ${e}`);
  if (confg.egress_policy === "block") {
    core.info("Switching egress-policy to audit mode");   // ← fail OPEN
    confg.egress_policy = "audit";
  }
}

When the cache entry is absent — a cold cache, or (commonly) when the bootstrapping saveCache a few lines above loses the reservation race (Unable to reserve cache with key harden-runner-cacheKey, another job may be creating this cache) so the entry is never created — GetCacheEntryDownloadURL returns no signedDownloadUrl, new URL(undefined) throws TypeError: Invalid URL, and the catch sets egress_policy = "audit". The job then runs with no egress enforcement while the workflow says block, and the only signal is one info line. That's a fail-open in a security control (details and repro in #675). The v1 branch has the identical pattern.

The change

Both the v2 and v1 cache-service branches now keep block and emit a core.warning on a cache-host resolution miss, instead of downgrading. The cache host is only needed to allowlist harden-runner's own cache traffic; failing to resolve it must not disable the user's configured egress policy. (Left the policy-store 5xx fallback untouched — that's a separate path.)

Validation

Reproduced and fixed on a repo that hits the reservation race, running a burst of concurrent block-mode jobs (example.com egress canary; example.com is never allowlisted, so connected = enforcement is off):

Action Arm path Agent EgressPolicy Canary
step-security/harden-runner@v2.20.0 lost the reserve race → cold fetch audit connected (leak)
this branch lost the reserve race → cold fetch block could not connect (enforced)

Same repo, same race, same commit — the official action downgraded to audit and let egress through; this branch kept block and the canary was blocked. The jobs that won the reservation race resolved the cache host and held block on both, unchanged. dist/ is rebuilt (npm run build).

Note

I've implemented this as fail-closed by default, since silently disabling a configured security control is the bug. If you'd prefer it gated behind an opt-in input (e.g. fail-on-policy-downgrade: true, as floated in #675), happy to adjust.

Warning

LLM Disclosure

This PR was authored by claude-opus-4-8 on behalf of @JPHutchins, after tracing the fail-open in dist/pre/index.js/src/setup.ts and validating the fix against the reservation-race repro described in #675.

Under egress-policy: block, harden-runner fetches the harden-runner-cacheKey
cache entry only to discover the cache blob host and add it to the allowlist.
When that entry does not exist (a cold cache, or a repo whose bootstrap
saveCache lost the reserve race), GetCacheEntryDownloadURL returns no download
URL, so `new URL(undefined)` throws and the catch silently downgraded
block -> audit. The job then ran with no egress enforcement while the YAML
said block: a fail-open in a security control (step-security#675).

A cache-host resolution miss is a normal, expected condition and must not
disable the user's egress policy. Keep block (fail closed) and emit a warning
in both the v2 and v1 cache-service branches; the cache host is only needed to
allowlist harden-runner's own cache traffic.

Refs: step-security#675

Co-Authored-By: claude-opus-4-8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qgqy11vvMbUtCmrBML85sD
@JPHutchins

Copy link
Copy Markdown
Author

Moved to draft to avoid LLM slop merge. Consider as proof of concept.

JPHutchins added a commit to JPHutchins/code-review that referenced this pull request Jul 15, 2026
…rges

harden-runner's egress-policy: block silently degrades to audit when it cannot
resolve the Actions cache host during arming (a cold cache, or a lost cache
reservation race): GetCacheEntryDownloadURL returns no URL, new URL(undefined)
throws, and the catch downgrades block -> audit. The job then runs with no
egress enforcement while the YAML says block (step-security/harden-runner#675).
The #72 canary catches this and fails closed, so review jobs on repos with a
cold harden-runner cache (e.g. jphfmt) get no review at all.

Pin both harden-runner steps in the reusable workflow and the copy-paste
example to JPHutchins/harden-runner@ee143ec (v2.20.0 + the fail-closed fix),
which keeps block instead of downgrading. Validated on jphfmt: on the cold
cache-reservation race the official action downgrades to audit and leaks egress
while the fork keeps block and the egress canary is blocked. Fix is PR'd
upstream (step-security/harden-runner#676); revert to the fixed official
release once merged.

Refs: #79, step-security/harden-runner#675, step-security/harden-runner#676

Co-Authored-By: claude-opus-4-8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qgqy11vvMbUtCmrBML85sD
JPHutchins added a commit to JPHutchins/code-review that referenced this pull request Jul 15, 2026
…rges (#80)

harden-runner's egress-policy: block silently degrades to audit when it cannot
resolve the Actions cache host during arming (a cold cache, or a lost cache
reservation race): GetCacheEntryDownloadURL returns no URL, new URL(undefined)
throws, and the catch downgrades block -> audit. The job then runs with no
egress enforcement while the YAML says block (step-security/harden-runner#675).
The #72 canary catches this and fails closed, so review jobs on repos with a
cold harden-runner cache (e.g. jphfmt) get no review at all.

Pin both harden-runner steps in the reusable workflow and the copy-paste
example to JPHutchins/harden-runner@ee143ec (v2.20.0 + the fail-closed fix),
which keeps block instead of downgrading. Validated on jphfmt: on the cold
cache-reservation race the official action downgrades to audit and leaks egress
while the fork keeps block and the egress canary is blocked. Fix is PR'd
upstream (step-security/harden-runner#676); revert to the fixed official
release once merged.

Refs: #79, step-security/harden-runner#675, step-security/harden-runner#676


Claude-Session: https://claude.ai/code/session_01Qgqy11vvMbUtCmrBML85sD

Co-authored-by: claude-opus-4-8 <noreply@anthropic.com>
JPHutchins added a commit to JPHutchins/code-review that referenced this pull request Jul 15, 2026
Pins harden-runner to the fail-closed fork (JPHutchins/harden-runner@ee143ec)
until step-security/harden-runner#676 merges, so block-mode reviews on repos
with a cold harden-runner cache (e.g. jphfmt) enforce egress instead of
fail-closing at the canary. See #79.

Co-Authored-By: claude-opus-4-8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qgqy11vvMbUtCmrBML85sD
JPHutchins added a commit to JPHutchins/jphfmt that referenced this pull request Jul 15, 2026
Pins the fail-closed harden-runner fork (JPHutchins/harden-runner@ee143ec) so
block-mode egress enforces on a cold harden-runner cache instead of silently
degrading to audit. See JPHutchins/code-review#79 and
step-security/harden-runner#676.

Co-Authored-By: claude-opus-4-8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qgqy11vvMbUtCmrBML85sD
JPHutchins added a commit to JPHutchins/camas that referenced this pull request Jul 15, 2026
Pins the fail-closed harden-runner fork (JPHutchins/harden-runner@ee143ec) so
block-mode egress enforces on a cold harden-runner cache instead of silently
degrading to audit. See JPHutchins/code-review#79 and
step-security/harden-runner#676.

Co-Authored-By: claude-opus-4-8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qgqy11vvMbUtCmrBML85sD
JPHutchins added a commit to JPHutchins/cerive that referenced this pull request Jul 15, 2026
Pins the fail-closed harden-runner fork (JPHutchins/harden-runner@ee143ec) so
block-mode egress enforces on a cold harden-runner cache instead of silently
degrading to audit. See JPHutchins/code-review#79 and
step-security/harden-runner#676.

Co-Authored-By: claude-opus-4-8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qgqy11vvMbUtCmrBML85sD
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.

egress-policy: block silently degrades to audit when cacheURL resolution throws (fail-open in a security control)

1 participant