diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 2127cf132..50bb19d60 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -133,18 +133,22 @@ ], packageRules: [ { - // The Renovate runner image (bfra-me/renovate-action) curl-installs Bun - // globally as root and runs Renovate as a non-root user, so the bun - // manager's lockfile regeneration (`install-tool bun ` → - // updateArtifacts) fails EACCES writing the root-owned containerbase tool - // dir — failing the `renovate/artifacts` check on every branch, for any - // version. Skip Renovate's bun artifact update; the `postUpgradeTasks` - // `bun install` below regenerates bun.lock on the runner's installed Bun, - // so the lockfile still stays current. Dependency update PRs are - // unaffected. Remove if the runner switches to binarySource=global. - matchManagers: ['bun'], + // bfra-me/renovate-action sets RENOVATE_BINARY_SOURCE=install, so + // Renovate's built-in artifact update path calls `install-tool bun ` + // before regenerating bun.lock. That fails in this environment; the + // postUpgradeTasks `bun install` below regenerates bun.lock instead. + matchManagers: ['bun', 'npm'], skipArtifactsUpdate: true, }, + { + // @types/node must not be grouped with Docker/custom-manager Node surfaces: + // skipArtifactsUpdate only suppresses artifact regeneration when every + // upgrade in the group carries the flag, so mixed-manager Node groups still + // hit the failing bun installer path. + matchManagers: ['npm'], + matchPackageNames: ['@types/node'], + groupName: null, + }, {matchFileNames: ['.github/workflows/**'], semanticCommitType: 'ci'}, {matchDatasources: ['docker'], semanticCommitType: 'build'}, { diff --git a/.node-version b/.node-version index 1dd37d537..ca5c35005 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -24.17.0 +24.18.0 diff --git a/bun.lock b/bun.lock index 4669c4300..92f0ab3f4 100644 --- a/bun.lock +++ b/bun.lock @@ -24,7 +24,7 @@ "@octokit/webhooks-types": "7.6.1", "@semantic-release/exec": "7.1.0", "@semantic-release/git": "10.0.1", - "@types/node": "24.13.1", + "@types/node": "24.13.2", "@vitest/eslint-plugin": "1.6.20", "conventional-changelog-conventionalcommits": "9.3.1", "eslint": "10.5.0", @@ -488,7 +488,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.13.1", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg=="], + "@types/node": ["@types/node@24.13.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA=="], "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], @@ -1704,6 +1704,8 @@ "@stylistic/eslint-plugin/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + "@types/ws/@types/node": ["@types/node@24.13.1", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="], "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg=="], diff --git a/deploy/compose.yaml b/deploy/compose.yaml index 83caba63e..6aefeebaa 100644 --- a/deploy/compose.yaml +++ b/deploy/compose.yaml @@ -201,6 +201,15 @@ services: - gateway-net - sandbox-net restart: unless-stopped + # Container hardening (#1053). The gateway binds the operator listener on an + # unprivileged port (4000) and its healthcheck uses `nc -z` (TCP connect, not + # a raw socket), so it needs no added capabilities — drop the full set. Root + # filesystem read-only and non-root user are deferred (the runtime writes a + # readiness flag to /var/run/fro-bot; that needs a tmpfs/write-path audit first). + cap_drop: + - ALL + security_opt: + - no-new-privileges:true logging: driver: json-file options: diff --git a/deploy/gateway.Dockerfile b/deploy/gateway.Dockerfile index c175333b4..b06f0a739 100644 --- a/deploy/gateway.Dockerfile +++ b/deploy/gateway.Dockerfile @@ -1,5 +1,5 @@ # ── Stage 1: build ──────────────────────────────────────────────────────────── -FROM node:24.17.0-alpine@sha256:156b55f92e98ccd5ef49578a8cea0df4679826564bad1c9d4ef04462b9f0ded6 AS build +FROM node:24.18.0-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS build WORKDIR /workspace @@ -100,7 +100,7 @@ RUN rm -rf node_modules apps/*/node_modules packages/*/node_modules \ && bun install --production --frozen-lockfile --ignore-scripts # ── Stage 2: runtime ────────────────────────────────────────────────────────── -FROM node:24.17.0-alpine@sha256:156b55f92e98ccd5ef49578a8cea0df4679826564bad1c9d4ef04462b9f0ded6 AS runtime +FROM node:24.18.0-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS runtime WORKDIR /app diff --git a/deploy/validate-stack.test.sh b/deploy/validate-stack.test.sh index e11b6eb25..ca1055d6f 100644 --- a/deploy/validate-stack.test.sh +++ b/deploy/validate-stack.test.sh @@ -4656,6 +4656,71 @@ echo "" echo " OP-12 output (stderr+stdout combined):" echo "${OP12_OUTPUT}" | sed 's/^/ /' +# --------------------------------------------------------------------------- +# TEST 68 — Positive regression: real deploy/compose.yaml gateway service must +# declare cap_drop: [ALL] and security_opt: [no-new-privileges:true] +# (#1053 container hardening). +# +# This test asserts directly against the real compose.yaml so that removing +# either hardening key from the gateway service causes an immediate test +# failure. It does NOT use validate-stack.sh (which does not check cap_drop/ +# security_opt on the gateway); instead it inspects the raw YAML file with +# grep, matching the exact indented forms that Compose requires. +# --------------------------------------------------------------------------- +echo "" +echo "--- TEST 68: gateway service in real compose.yaml declares cap_drop: [ALL] and security_opt: [no-new-privileges:true] (#1053) ---" + +REAL_COMPOSE_FILE="deploy/compose.yaml" + +# #given the real compose.yaml exists +# #when we inspect the GATEWAY service block (not the whole file) for cap_drop +# and security_opt. Extract only the gateway service block — from the ` gateway:` +# line up to the next top-level ` :` line — so the assertions cannot +# false-pass on keys that belong to another service (workspace/mitmproxy). +GW_BLOCK="$(awk ' + /^ gateway:[[:space:]]*$/ { in_block = 1; next } + in_block && /^ [a-zA-Z0-9_-]+:[[:space:]]*$/ { in_block = 0 } + in_block { print } +' "${REAL_COMPOSE_FILE}")" + +# cap_drop: - ALL (4-space indent for key, 6-space for list item) +GW_CAP_DROP_EXIT=0 +grep -q "^ cap_drop:" <<<"${GW_BLOCK}" || GW_CAP_DROP_EXIT=$? +GW_CAP_DROP_ALL_EXIT=0 +grep -q "^ - ALL" <<<"${GW_BLOCK}" || GW_CAP_DROP_ALL_EXIT=$? + +# security_opt: - no-new-privileges:true (4-space indent for key, 6-space for list item) +GW_SECOPT_EXIT=0 +grep -q "^ security_opt:" <<<"${GW_BLOCK}" || GW_SECOPT_EXIT=$? +GW_SECOPT_NNP_EXIT=0 +grep -q "^ - no-new-privileges:true" <<<"${GW_BLOCK}" || GW_SECOPT_NNP_EXIT=$? + +# #then both hardening keys must be present in the gateway service + +if [[ "${GW_CAP_DROP_EXIT}" -eq 0 ]]; then + pass "TEST 68: real compose.yaml gateway service declares 'cap_drop:'" +else + fail "TEST 68: real compose.yaml gateway service is MISSING 'cap_drop:' — add cap_drop: [ALL] (#1053)" +fi + +if [[ "${GW_CAP_DROP_ALL_EXIT}" -eq 0 ]]; then + pass "TEST 68: real compose.yaml gateway service declares 'cap_drop: - ALL'" +else + fail "TEST 68: real compose.yaml gateway service is MISSING '- ALL' under cap_drop — add cap_drop: [ALL] (#1053)" +fi + +if [[ "${GW_SECOPT_EXIT}" -eq 0 ]]; then + pass "TEST 68: real compose.yaml gateway service declares 'security_opt:'" +else + fail "TEST 68: real compose.yaml gateway service is MISSING 'security_opt:' — add security_opt: [no-new-privileges:true] (#1053)" +fi + +if [[ "${GW_SECOPT_NNP_EXIT}" -eq 0 ]]; then + pass "TEST 68: real compose.yaml gateway service declares 'security_opt: - no-new-privileges:true'" +else + fail "TEST 68: real compose.yaml gateway service is MISSING '- no-new-privileges:true' under security_opt — add security_opt: [no-new-privileges:true] (#1053)" +fi + # --------------------------------------------------------------------------- # LOCKSTEP-1 — Negative/Positive: egress-smoke.sh must not hardcode the # mitmproxy image; it must derive it from deploy/compose.yaml. diff --git a/deploy/workspace.Dockerfile b/deploy/workspace.Dockerfile index 7af5fb380..16b19ec51 100644 --- a/deploy/workspace.Dockerfile +++ b/deploy/workspace.Dockerfile @@ -17,7 +17,7 @@ # packages/runtime/src/shared/constants.ts. # ── Stage 1: build ──────────────────────────────────────────────────────────── -FROM node:24.17.0-alpine@sha256:156b55f92e98ccd5ef49578a8cea0df4679826564bad1c9d4ef04462b9f0ded6 AS build +FROM node:24.18.0-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS build WORKDIR /workspace @@ -112,7 +112,7 @@ RUN rm -rf node_modules apps/*/node_modules packages/*/node_modules \ && bun install --production --frozen-lockfile --ignore-scripts # ── Stage 2: runtime ────────────────────────────────────────────────────────── -FROM node:24.17.0-alpine@sha256:156b55f92e98ccd5ef49578a8cea0df4679826564bad1c9d4ef04462b9f0ded6 AS runtime +FROM node:24.18.0-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS runtime WORKDIR /app diff --git a/package.json b/package.json index ef796844f..7928d9857 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@octokit/webhooks-types": "7.6.1", "@semantic-release/exec": "7.1.0", "@semantic-release/git": "10.0.1", - "@types/node": "24.13.1", + "@types/node": "24.13.2", "@vitest/eslint-plugin": "1.6.20", "conventional-changelog-conventionalcommits": "9.3.1", "eslint": "10.5.0",