From a1493500a3928f90f9f2fb6ecdc873c73759daaa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:32:02 +0000 Subject: [PATCH 1/3] Initial plan From 1a9aa1c3ac481790727864c2760df16a11199a3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:38:29 +0000 Subject: [PATCH 2/3] fix: strip raw network file capabilities --- containers/agent/Dockerfile | 11 +++++++++++ scripts/ci/agent-dockerfile-security.test.ts | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 scripts/ci/agent-dockerfile-security.test.ts diff --git a/containers/agent/Dockerfile b/containers/agent/Dockerfile index caa288a7b..6599bc1ff 100644 --- a/containers/agent/Dockerfile +++ b/containers/agent/Dockerfile @@ -106,6 +106,17 @@ RUN set -eux; \ apt_update_retry && \ apt_install_retry $PKGS && \ apt_upgrade_retry && \ + # Raw sockets are unavailable to the agent; remove stale file capabilities + # inherited from runner-like base images so they cannot be reactivated. + for binary in /usr/bin/ping /usr/bin/mtr-packet; do \ + if [ -e "$binary" ]; then \ + setcap -r "$binary" 2>/dev/null || true; \ + if getcap "$binary" | grep -q .; then \ + echo "ERROR: Failed to remove file capabilities from $binary" >&2; \ + exit 1; \ + fi; \ + fi; \ + done && \ # Install gosu 1.19 from GitHub releases (compiled with Go 1.24.6 — avoids Ubuntu's # apt gosu which is compiled with Go 1.18 and carries 44 stdlib CVEs including Critical ones). # GO-2026-4337 (TLS session resumption in Go stdlib) is reported against this binary by diff --git a/scripts/ci/agent-dockerfile-security.test.ts b/scripts/ci/agent-dockerfile-security.test.ts new file mode 100644 index 000000000..e70c4aac3 --- /dev/null +++ b/scripts/ci/agent-dockerfile-security.test.ts @@ -0,0 +1,18 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +const dockerfilePath = path.resolve(__dirname, '../../containers/agent/Dockerfile'); + +describe('agent Dockerfile security hardening', () => { + it('strips and verifies file capabilities on raw-network utilities', () => { + const dockerfile = fs.readFileSync(dockerfilePath, 'utf-8'); + const hardeningBlock = dockerfile.match( + /for binary in \/usr\/bin\/ping \/usr\/bin\/mtr-packet; do[\s\S]*?done && \\/ + )?.[0]; + + expect(hardeningBlock).toBeDefined(); + expect(hardeningBlock).toContain('setcap -r "$binary"'); + expect(hardeningBlock).toContain('getcap "$binary"'); + expect(hardeningBlock).toContain('exit 1'); + }); +}); From 84bf722e05ff930abf17837c3842fcdf72e6d1df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:16:39 +0000 Subject: [PATCH 3/3] fix: fail closed stripping raw network caps --- containers/agent/Dockerfile | 17 ++- scripts/ci/agent-dockerfile-security.test.ts | 115 ++++++++++++++++++- 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/containers/agent/Dockerfile b/containers/agent/Dockerfile index 6599bc1ff..5cc45cf1a 100644 --- a/containers/agent/Dockerfile +++ b/containers/agent/Dockerfile @@ -110,8 +110,21 @@ RUN set -eux; \ # inherited from runner-like base images so they cannot be reactivated. for binary in /usr/bin/ping /usr/bin/mtr-packet; do \ if [ -e "$binary" ]; then \ - setcap -r "$binary" 2>/dev/null || true; \ - if getcap "$binary" | grep -q .; then \ + if ! before_caps="$(getcap "$binary")"; then \ + echo "ERROR: Failed to inspect file capabilities on $binary" >&2; \ + exit 1; \ + fi; \ + if [ -n "$before_caps" ]; then \ + if ! setcap -r "$binary"; then \ + echo "ERROR: Failed to remove file capabilities from $binary" >&2; \ + exit 1; \ + fi; \ + fi; \ + if ! after_caps="$(getcap "$binary")"; then \ + echo "ERROR: Failed to inspect file capabilities on $binary after removal" >&2; \ + exit 1; \ + fi; \ + if [ -n "$after_caps" ]; then \ echo "ERROR: Failed to remove file capabilities from $binary" >&2; \ exit 1; \ fi; \ diff --git a/scripts/ci/agent-dockerfile-security.test.ts b/scripts/ci/agent-dockerfile-security.test.ts index e70c4aac3..8c4fa63c2 100644 --- a/scripts/ci/agent-dockerfile-security.test.ts +++ b/scripts/ci/agent-dockerfile-security.test.ts @@ -1,18 +1,125 @@ import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; +import { spawnSync } from 'child_process'; const dockerfilePath = path.resolve(__dirname, '../../containers/agent/Dockerfile'); describe('agent Dockerfile security hardening', () => { - it('strips and verifies file capabilities on raw-network utilities', () => { + const readHardeningScript = (fixtureDir: string): string => { const dockerfile = fs.readFileSync(dockerfilePath, 'utf-8'); const hardeningBlock = dockerfile.match( /for binary in \/usr\/bin\/ping \/usr\/bin\/mtr-packet; do[\s\S]*?done && \\/ )?.[0]; expect(hardeningBlock).toBeDefined(); - expect(hardeningBlock).toContain('setcap -r "$binary"'); - expect(hardeningBlock).toContain('getcap "$binary"'); - expect(hardeningBlock).toContain('exit 1'); + const pingPath = path.join(fixtureDir, 'ping'); + const mtrPacketPath = path.join(fixtureDir, 'mtr-packet'); + + return hardeningBlock! + .replace(/done && \\\s*$/, 'done') + .replace(/\/usr\/bin\/ping/g, pingPath) + .replace(/\/usr\/bin\/mtr-packet/g, mtrPacketPath) + .split('\n') + .map((line) => line.trim().replace(/\s*\\$/, '')) + .join('\n'); + }; + + const runHardeningScript = ( + env: NodeJS.ProcessEnv = {} + ): { stderr: string; status: number | null; stubDir: string } => { + const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'awf-agent-hardening-')); + fs.writeFileSync(path.join(fixtureDir, 'ping'), ''); + fs.writeFileSync(path.join(fixtureDir, 'mtr-packet'), ''); + + const stubDir = fs.mkdtempSync(path.join(os.tmpdir(), 'awf-agent-hardening-stubs-')); + const script = ` +set -e +getcap() { + echo "$1" >> "${stubDir}/getcap-calls" + case "$GETCAP_BEHAVIOR" in + fail) return 7 ;; + none) return 0 ;; + capability-then-none) + if [ "$(wc -l < "${stubDir}/getcap-calls")" -eq 1 ]; then + echo "$1 cap_net_raw=ep" + fi + return 0 + ;; + always-capability) + echo "$1 cap_net_raw=ep" + return 0 + ;; + esac +} +setcap() { + echo "$*" >> "${stubDir}/setcap-calls" + case "$SETCAP_BEHAVIOR" in + fail) return 8 ;; + *) return 0 ;; + esac +} +${readHardeningScript(fixtureDir)} +`; + + const result = spawnSync('bash', ['-c', script], { + encoding: 'utf-8', + env: { ...process.env, ...env }, + }); + + fs.rmSync(fixtureDir, { recursive: true, force: true }); + return { stderr: result.stderr, status: result.status, stubDir }; + }; + + afterEach(() => { + for (const entry of fs.readdirSync(os.tmpdir())) { + if (entry.startsWith('awf-agent-hardening-stubs-')) { + fs.rmSync(path.join(os.tmpdir(), entry), { recursive: true, force: true }); + } + } + }); + + it('strips and verifies file capabilities on raw-network utilities', () => { + const script = readHardeningScript('/tmp'); + + expect(script).toContain('for binary in /tmp/ping /tmp/mtr-packet; do'); + expect(script).toContain('before_caps="$(getcap "$binary")"'); + expect(script).toContain('setcap -r "$binary"'); + expect(script).toContain('after_caps="$(getcap "$binary")"'); + expect(script).toContain('exit 1'); + expect(script).not.toMatch(/setcap -r "\$binary"[^;\n]*\|\| true/); + }); + + it('fails closed when capability inspection fails', () => { + const result = runHardeningScript({ GETCAP_BEHAVIOR: 'fail' }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain('ERROR: Failed to inspect file capabilities'); + }); + + it('fails closed when capability removal fails', () => { + const result = runHardeningScript({ + GETCAP_BEHAVIOR: 'always-capability', + SETCAP_BEHAVIOR: 'fail', + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain('ERROR: Failed to remove file capabilities'); + }); + + it('removes capabilities only after inspection proves they are present', () => { + const result = runHardeningScript({ GETCAP_BEHAVIOR: 'none' }); + + expect(result.status).toBe(0); + expect(fs.existsSync(path.join(result.stubDir, 'setcap-calls'))).toBe(false); + }); + + it('passes when capability removal succeeds and verification is clean', () => { + const result = runHardeningScript({ GETCAP_BEHAVIOR: 'capability-then-none' }); + + expect(result.status).toBe(0); + expect(fs.readFileSync(path.join(result.stubDir, 'setcap-calls'), 'utf-8')).toContain( + '-r ' + ); }); });