fix: resolve cache host read-first and never downgrade egress policy

Look up the harden-runner cache entry before seeding it, so the
constant-key reservation no longer fails with a misleading log line on
every run after the first (#681). Guard the v2 lookup against the
cache-miss response ({ok: false, signedDownloadUrl: ""}) whose empty
URL previously threw, and seed the entry only on a miss, retrying the
lookup once afterward.

When the host still cannot be resolved, keep egress-policy set to
block instead of silently switching to audit (#675). The agents allow
the Actions cache endpoints implicitly, so this lookup is a
defense-in-depth addition and its failure is safe to ignore.

Fixes #675. Fixes #681.
This commit is contained in:
Varun Sharma
2026-08-14 17:48:29 -07:00
parent e7399dd3e9
commit 93b58ee491
3 changed files with 159 additions and 136 deletions
+76 -54
View File
@@ -85935,6 +85935,76 @@ process.on("unhandledRejection", (reason) => {
const detail = reason instanceof Error ? ((_a = reason.stack) !== null && _a !== void 0 ? _a : reason.message) : String(reason);
lib_core.warning(`Unhandled promise rejection during Pre-step: ${detail}`);
});
// Looks up the Actions cache blob host for the harden-runner cache entry.
// Returns "<hostname>:443", or undefined when the entry does not exist yet.
function lookupCacheHost() {
return setup_awaiter(this, void 0, void 0, function* () {
const cacheFilePath = external_path_.join(__dirname, "cache.txt");
const compressionMethod = yield cacheUtils.getCompressionMethod();
const cacheServiceVersion = (0,config.getCacheServiceVersion)();
if (cacheServiceVersion === "v2") {
const twirpClient = cacheTwirpClient.internalCacheTwirpClient();
const request = {
key: cacheKey,
restoreKeys: [],
version: cacheUtils.getCacheVersion([cacheFilePath], compressionMethod, false),
};
const response = yield twirpClient.GetCacheEntryDownloadURL(request);
// On a cache miss the twirp response is {ok: false, signedDownloadUrl: ""};
// constructing a URL from that empty string would throw.
if (!response.ok || !response.signedDownloadUrl) {
return undefined;
}
return `${new URL(response.signedDownloadUrl).hostname}:443`;
}
if (cacheServiceVersion === "v1") {
const cacheEntry = yield (0,cacheHttpClient.getCacheEntry)([cacheKey], [cacheFilePath], { compressionMethod });
// On a cache miss getCacheEntry returns null.
if (!(cacheEntry === null || cacheEntry === void 0 ? void 0 : cacheEntry.archiveLocation)) {
return undefined;
}
return `${new URL(cacheEntry.archiveLocation).hostname}:443`;
}
return undefined;
});
}
// Resolves the Actions cache blob host, seeding the harden-runner cache entry
// on the first run in a cache scope. Read-first: the constant-key entry can
// only be reserved once per scope, so an unconditional save would log a
// misleading reservation failure on every subsequent run. Returns undefined
// when the host cannot be resolved; callers must not change the egress policy
// in that case, the agent allows the cache endpoints implicitly and this
// lookup is defense-in-depth only.
function resolveCacheHost() {
return setup_awaiter(this, void 0, void 0, function* () {
lib_core.info(`cache version: ${(0,config.getCacheServiceVersion)()}`);
try {
const host = yield lookupCacheHost();
if (host) {
return host;
}
}
catch (e) {
lib_core.info(`Unable to fetch cacheURL ${e}`);
}
// First run in this cache scope: seed the entry, then look up again. If a
// concurrent job wins the reservation race, saveCache fails harmlessly and
// the second lookup reads the winner's entry.
try {
yield cache.saveCache([external_path_.join(__dirname, "cache.txt")], cacheKey);
}
catch (exception) {
lib_core.info(`Unable to seed cache entry: ${exception}`);
}
try {
return yield lookupCacheHost();
}
catch (e) {
lib_core.info(`Unable to fetch cacheURL ${e}`);
return undefined;
}
});
}
(() => setup_awaiter(void 0, void 0, void 0, function* () {
var _a, _b, _c, _d;
try {
@@ -86071,61 +86141,13 @@ process.on("unhandledRejection", (reason) => {
if (isValidEvent() &&
confg.egress_policy === "block" &&
!isDenyListMode) {
try {
const cacheResult = yield cache.saveCache([external_path_.join(__dirname, "cache.txt")], cacheKey);
console.log(cacheResult);
const cacheHost = yield resolveCacheHost();
if (cacheHost) {
lib_core.info(`Adding cacheHost: ${cacheHost} to allowed-endpoints`);
confg.allowed_endpoints += ` ${cacheHost}`;
}
catch (exception) {
console.log(exception);
}
const cacheServiceVersion = (0,config.getCacheServiceVersion)();
switch (cacheServiceVersion) {
case "v2":
lib_core.info(`cache version: v2`);
try {
const cacheFilePath = external_path_.join(__dirname, "cache.txt");
lib_core.info(`cacheFilePath ${cacheFilePath}`);
const twirpClient = cacheTwirpClient.internalCacheTwirpClient();
const compressionMethod = yield cacheUtils.getCompressionMethod();
const request = {
key: cacheKey,
restoreKeys: [],
version: cacheUtils.getCacheVersion([cacheFilePath], compressionMethod, false),
};
const response = yield twirpClient.GetCacheEntryDownloadURL(request);
const url = new URL(response.signedDownloadUrl);
lib_core.info(`Adding cacheHost: ${url.hostname}:443 to allowed-endpoints`);
confg.allowed_endpoints += ` ${url.hostname}:443`;
}
catch (e) {
lib_core.info(`Unable to fetch cacheURL ${e}`);
if (confg.egress_policy === "block") {
lib_core.info("Switching egress-policy to audit mode");
confg.egress_policy = "audit";
}
}
break;
case "v1":
lib_core.info(`cache version: v1`);
try {
const compressionMethod = yield cacheUtils.getCompressionMethod();
const cacheFilePath = external_path_.join(__dirname, "cache.txt");
lib_core.info(`cacheFilePath ${cacheFilePath}`);
const cacheEntry = yield (0,cacheHttpClient.getCacheEntry)([cacheKey], [cacheFilePath], {
compressionMethod: compressionMethod,
});
const url = new URL(cacheEntry.archiveLocation);
lib_core.info(`Adding cacheHost: ${url.hostname}:443 to allowed-endpoints`);
confg.allowed_endpoints += ` ${url.hostname}:443`;
}
catch (exception) {
// some exception has occurred.
lib_core.info(`Unable to fetch cacheURL ${exception}`);
if (confg.egress_policy === "block") {
lib_core.info("Switching egress-policy to audit mode");
confg.egress_policy = "audit";
}
}
else {
lib_core.info("Unable to resolve the Actions cache host. This should not cause any problems: the agent allows the cache endpoints implicitly, and this lookup is defense-in-depth only. Egress policy remains block.");
}
}
if (!confg.disable_telemetry || confg.egress_policy === "audit") {
+1 -1
View File
File diff suppressed because one or more lines are too long
+82 -81
View File
@@ -11,7 +11,6 @@ import {
ArtifactCacheEntry,
cacheKey,
cacheFile,
CompressionMethod,
isValidEvent,
} from "./cache";
import { Configuration, PolicyResponse } from "./interfaces";
@@ -57,6 +56,81 @@ process.on("unhandledRejection", (reason) => {
core.warning(`Unhandled promise rejection during Pre-step: ${detail}`);
});
// Looks up the Actions cache blob host for the harden-runner cache entry.
// Returns "<hostname>:443", or undefined when the entry does not exist yet.
async function lookupCacheHost(): Promise<string | undefined> {
const cacheFilePath = path.join(__dirname, "cache.txt");
const compressionMethod = await utils.getCompressionMethod();
const cacheServiceVersion = getCacheServiceVersion();
if (cacheServiceVersion === "v2") {
const twirpClient = cacheTwirpClient.internalCacheTwirpClient();
const request: GetCacheEntryDownloadURLRequest = {
key: cacheKey,
restoreKeys: [],
version: utils.getCacheVersion([cacheFilePath], compressionMethod, false),
};
const response = await twirpClient.GetCacheEntryDownloadURL(request);
// On a cache miss the twirp response is {ok: false, signedDownloadUrl: ""};
// constructing a URL from that empty string would throw.
if (!response.ok || !response.signedDownloadUrl) {
return undefined;
}
return `${new URL(response.signedDownloadUrl).hostname}:443`;
}
if (cacheServiceVersion === "v1") {
const cacheEntry: ArtifactCacheEntry = await getCacheEntry(
[cacheKey],
[cacheFilePath],
{ compressionMethod }
);
// On a cache miss getCacheEntry returns null.
if (!cacheEntry?.archiveLocation) {
return undefined;
}
return `${new URL(cacheEntry.archiveLocation).hostname}:443`;
}
return undefined;
}
// Resolves the Actions cache blob host, seeding the harden-runner cache entry
// on the first run in a cache scope. Read-first: the constant-key entry can
// only be reserved once per scope, so an unconditional save would log a
// misleading reservation failure on every subsequent run. Returns undefined
// when the host cannot be resolved; callers must not change the egress policy
// in that case, the agent allows the cache endpoints implicitly and this
// lookup is defense-in-depth only.
async function resolveCacheHost(): Promise<string | undefined> {
core.info(`cache version: ${getCacheServiceVersion()}`);
try {
const host = await lookupCacheHost();
if (host) {
return host;
}
} catch (e) {
core.info(`Unable to fetch cacheURL ${e}`);
}
// First run in this cache scope: seed the entry, then look up again. If a
// concurrent job wins the reservation race, saveCache fails harmlessly and
// the second lookup reads the winner's entry.
try {
await cache.saveCache([path.join(__dirname, "cache.txt")], cacheKey);
} catch (exception) {
core.info(`Unable to seed cache entry: ${exception}`);
}
try {
return await lookupCacheHost();
} catch (e) {
core.info(`Unable to fetch cacheURL ${e}`);
return undefined;
}
}
(async () => {
try {
console.log("[harden-runner] pre-step");
@@ -229,87 +303,14 @@ process.on("unhandledRejection", (reason) => {
confg.egress_policy === "block" &&
!isDenyListMode
) {
try {
const cacheResult = await cache.saveCache(
[path.join(__dirname, "cache.txt")],
cacheKey
const cacheHost = await resolveCacheHost();
if (cacheHost) {
core.info(`Adding cacheHost: ${cacheHost} to allowed-endpoints`);
confg.allowed_endpoints += ` ${cacheHost}`;
} else {
core.info(
"Unable to resolve the Actions cache host. This should not cause any problems: the agent allows the cache endpoints implicitly, and this lookup is defense-in-depth only. Egress policy remains block."
);
console.log(cacheResult);
} catch (exception) {
console.log(exception);
}
const cacheServiceVersion: string = getCacheServiceVersion();
switch (cacheServiceVersion) {
case "v2":
core.info(`cache version: v2`);
try {
const cacheFilePath = path.join(__dirname, "cache.txt");
core.info(`cacheFilePath ${cacheFilePath}`);
const twirpClient = cacheTwirpClient.internalCacheTwirpClient();
const compressionMethod = await utils.getCompressionMethod();
const request: GetCacheEntryDownloadURLRequest = {
key: cacheKey,
restoreKeys: [],
version: utils.getCacheVersion(
[cacheFilePath],
compressionMethod,
false
),
};
const response = await twirpClient.GetCacheEntryDownloadURL(
request
);
const url = new URL(response.signedDownloadUrl);
core.info(
`Adding cacheHost: ${url.hostname}:443 to allowed-endpoints`
);
confg.allowed_endpoints += ` ${url.hostname}:443`;
} catch (e) {
core.info(`Unable to fetch cacheURL ${e}`);
if (confg.egress_policy === "block") {
core.info("Switching egress-policy to audit mode");
confg.egress_policy = "audit";
}
}
break;
case "v1":
core.info(`cache version: v1`);
try {
const compressionMethod: CompressionMethod =
await utils.getCompressionMethod();
const cacheFilePath = path.join(__dirname, "cache.txt");
core.info(`cacheFilePath ${cacheFilePath}`);
const cacheEntry: ArtifactCacheEntry = await getCacheEntry(
[cacheKey],
[cacheFilePath],
{
compressionMethod: compressionMethod,
}
);
const url = new URL(cacheEntry.archiveLocation);
core.info(
`Adding cacheHost: ${url.hostname}:443 to allowed-endpoints`
);
confg.allowed_endpoints += ` ${url.hostname}:443`;
} catch (exception) {
// some exception has occurred.
core.info(`Unable to fetch cacheURL ${exception}`);
if (confg.egress_policy === "block") {
core.info("Switching egress-policy to audit mode");
confg.egress_policy = "audit";
}
}
}
}