Skip to content

Commit 91bd673

Browse files
lpcoxCopilotCopilot
authored
fix: share mcpg network namespace to fix TLS hostname verification (#1778)
* fix: bind mcpg to assigned IP + fail-close on missing GH_TOKEN Address security review findings from #1778: 1. Bind mcpg to its assigned IP (172.30.0.51) instead of 0.0.0.0 so the agent container cannot reach mcpg directly. Previously mcpg listened on all interfaces, making it reachable from any container on awf-net. 2. Add fail-close guard: generateDockerCompose now throws if enableCliProxy is set but githubToken is absent. mcpg requires a token to enforce DIFC policies — running without one would bypass integrity checks. 3. Use mcpg IP in healthcheck (not localhost) for TLS hostname consistency with how cli-proxy connects via GH_HOST. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: align TLS hostname by sharing mcpg network namespace Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/b1a5ac57-6103-45c6-b689-67924f7df25b * fix: remove duplicate comment block in docker-manager.ts Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/b1a5ac57-6103-45c6-b689-67924f7df25b * fix: add retry logic to apt-get upgrade in agent Dockerfile (#1781) Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/1831b666-eb93-4772-9455-4604a64bfd24 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 92f386c commit 91bd673

File tree

11 files changed

+306
-161
lines changed

11 files changed

+306
-161
lines changed

containers/agent/Dockerfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ RUN set -eux; \
5050

5151
# Upgrade all packages to pick up security patches
5252
# Addresses CVE-2023-44487 (HTTP/2 Rapid Reset) and other known vulnerabilities
53-
RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*
53+
# Retry logic handles transient mirror sync failures during apt-get update
54+
RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/* || \
55+
(echo "apt-get upgrade failed, retrying with fresh package index..." && \
56+
rm -rf /var/lib/apt/lists/* && \
57+
apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*)
5458

5559
# Create non-root user with UID/GID matching host user
5660
# This allows the user command to run with appropriate permissions

containers/cli-proxy/Dockerfile

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
1-
# CLI Proxy sidecar for AWF - provides gh CLI access with mcpg DIFC proxy
1+
# CLI Proxy sidecar for AWF - provides gh CLI access via mcpg DIFC proxy
22
#
3-
# Multi-stage build:
4-
# Stage 1: Extract mcpg binary from ghcr.io/github/gh-aw-mcpg image
5-
# Stage 2: Assemble final image with gh CLI, Node.js, and mcpg
6-
#
7-
# This container runs two processes:
8-
# 1. mcpg proxy (TLS, port 18443) - holds GH_TOKEN, enforces guard policies
9-
# 2. HTTP server (port 11000) - receives gh invocations from the agent container
10-
11-
# Stage 1: Extract the mcpg binary from the official gh-aw-mcpg image.
12-
# MCPG_IMAGE is configurable via --cli-proxy-mcpg-image so the AWF compiler
13-
# can control which mcpg version is pulled and run (e.g. for version pinning
14-
# or testing a new mcpg release before it is bundled in the GHCR cli-proxy image).
15-
ARG MCPG_IMAGE=ghcr.io/github/gh-aw-mcpg:v0.2.15
16-
FROM ${MCPG_IMAGE} AS mcpg-source
17-
18-
# Stage 2: Build the CLI proxy image
3+
# This container runs the HTTP exec server (port 11000) that receives gh
4+
# invocations from the agent container. The mcpg DIFC proxy runs as a
5+
# separate docker-compose service (awf-cli-proxy-mcpg) using the official
6+
# gh-aw-mcpg image directly — no binary extraction needed. GH_HOST is
7+
# set to the mcpg container so all gh CLI traffic flows through the proxy.
198
FROM node:22-alpine
209

2110
# Install gh CLI and curl for healthchecks/wrapper
@@ -26,10 +15,6 @@ RUN apk add --no-cache \
2615
ca-certificates \
2716
bash
2817

29-
# Copy the mcpg binary from the mcpg-source stage
30-
COPY --from=mcpg-source /usr/local/bin/mcpg /usr/local/bin/mcpg
31-
RUN chmod +x /usr/local/bin/mcpg
32-
3318
# Create app directory
3419
WORKDIR /app
3520

@@ -50,10 +35,10 @@ RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/healthcheck.sh
5035
RUN addgroup -S cliproxy && adduser -S cliproxy -G cliproxy
5136

5237
# Create log directory owned by cliproxy (so non-root process can write)
53-
RUN mkdir -p /var/log/cli-proxy/mcpg && \
38+
RUN mkdir -p /var/log/cli-proxy && \
5439
chown -R cliproxy:cliproxy /var/log/cli-proxy
5540

56-
# Create /tmp/proxy-tls directory owned by cliproxy for mcpg TLS cert generation
41+
# Create /tmp/proxy-tls directory owned by cliproxy for shared mcpg TLS certs
5742
RUN mkdir -p /tmp/proxy-tls && chown cliproxy:cliproxy /tmp/proxy-tls
5843

5944
# Switch to non-root user

containers/cli-proxy/entrypoint.sh

Lines changed: 16 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,24 @@
11
#!/bin/bash
22
# CLI Proxy sidecar entrypoint
3-
# Starts the mcpg DIFC proxy (GH_TOKEN required), then starts the Node.js HTTP server
4-
# under a supervisor loop so signals are properly handled and mcpg is cleaned up.
3+
#
4+
# The mcpg DIFC proxy runs as a separate docker-compose service
5+
# (awf-cli-proxy-mcpg). This container shares mcpg's network namespace
6+
# (network_mode: service:cli-proxy-mcpg), so localhost resolves to mcpg.
7+
# This ensures the TLS cert's SAN (localhost + 127.0.0.1) matches the
8+
# hostname used by the gh CLI, avoiding TLS hostname verification failures.
59
set -e
610

711
echo "[cli-proxy] Starting CLI proxy sidecar..."
812

9-
MCPG_PID=""
1013
NODE_PID=""
1114

12-
# GH_TOKEN is required: without it, mcpg cannot authenticate and DIFC guard policies
13-
# cannot be enforced. Fail closed rather than starting an unenforced server.
14-
if [ -z "$GH_TOKEN" ]; then
15-
echo "[cli-proxy] ERROR: GH_TOKEN not set - refusing to start without mcpg DIFC enforcement"
16-
exit 1
17-
fi
18-
19-
echo "[cli-proxy] GH_TOKEN present - starting mcpg DIFC proxy..."
15+
# cli-proxy shares mcpg's network namespace, so mcpg is always at localhost.
16+
# AWF_MCPG_PORT is set by docker-manager.ts.
17+
MCPG_PORT="${AWF_MCPG_PORT:-18443}"
2018

21-
mkdir -p /tmp/proxy-tls /var/log/cli-proxy/mcpg
19+
echo "[cli-proxy] mcpg proxy at localhost:${MCPG_PORT}"
2220

23-
# Build the guard policy JSON if not explicitly provided
24-
if [ -z "$AWF_GH_GUARD_POLICY" ]; then
25-
if [ -n "$GITHUB_REPOSITORY" ]; then
26-
AWF_GH_GUARD_POLICY="{\"repos\":[\"${GITHUB_REPOSITORY}\"],\"min-integrity\":\"public\"}"
27-
else
28-
AWF_GH_GUARD_POLICY="{\"min-integrity\":\"public\"}"
29-
fi
30-
echo "[cli-proxy] Using default guard policy: ${AWF_GH_GUARD_POLICY}"
31-
else
32-
echo "[cli-proxy] Using provided guard policy"
33-
fi
34-
35-
# Start mcpg proxy in background
36-
# mcpg proxy holds GH_TOKEN and applies DIFC guard policies before forwarding
37-
mcpg proxy \
38-
--policy "${AWF_GH_GUARD_POLICY}" \
39-
--listen 127.0.0.1:18443 \
40-
--tls \
41-
--tls-dir /tmp/proxy-tls \
42-
--guards-mode filter \
43-
--trusted-bots "github-actions[bot],github-actions,dependabot[bot],copilot" \
44-
--log-dir /var/log/cli-proxy/mcpg &
45-
MCPG_PID=$!
46-
echo "[cli-proxy] mcpg proxy started (PID: ${MCPG_PID})"
47-
48-
# Wait for TLS cert to be generated (max 30s)
21+
# Wait for TLS cert to appear in the shared volume (max 30s)
4922
echo "[cli-proxy] Waiting for mcpg TLS certificate..."
5023
i=0
5124
while [ $i -lt 30 ]; do
@@ -58,31 +31,26 @@ while [ $i -lt 30 ]; do
5831
done
5932

6033
if [ ! -f /tmp/proxy-tls/ca.crt ]; then
61-
echo "[cli-proxy] ERROR: mcpg TLS certificate not generated within 30s"
62-
kill "$MCPG_PID" 2>/dev/null || true
34+
echo "[cli-proxy] ERROR: mcpg TLS certificate not found within 30s"
6335
exit 1
6436
fi
6537

6638
# Configure gh CLI to route through the mcpg proxy (TLS, self-signed CA)
67-
export GH_HOST="localhost:18443"
39+
# Uses localhost because cli-proxy shares mcpg's network namespace — the
40+
# self-signed cert's SAN covers localhost, so TLS hostname verification passes.
41+
export GH_HOST="localhost:${MCPG_PORT}"
6842
export NODE_EXTRA_CA_CERTS="/tmp/proxy-tls/ca.crt"
6943
export GH_REPO="${GH_REPO:-$GITHUB_REPOSITORY}"
7044

7145
echo "[cli-proxy] gh CLI configured to route through mcpg proxy at ${GH_HOST}"
7246

73-
# Cleanup handler: stop both the Node HTTP server and mcpg when we receive a signal
74-
# or when the server exits. This runs correctly because we do NOT exec Node — we
75-
# start it in the background and wait, so the shell (and its traps) remain active.
47+
# Cleanup handler: stop the Node HTTP server on signal
7648
cleanup() {
7749
echo "[cli-proxy] Shutting down..."
7850
if [ -n "$NODE_PID" ]; then
7951
kill "$NODE_PID" 2>/dev/null || true
8052
wait "$NODE_PID" 2>/dev/null || true
8153
fi
82-
if [ -n "$MCPG_PID" ]; then
83-
kill "$MCPG_PID" 2>/dev/null || true
84-
wait "$MCPG_PID" 2>/dev/null || true
85-
fi
8654
}
8755
trap 'cleanup; exit 0' INT TERM
8856

scripts/ci/cleanup.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ echo "==========================================="
1212

1313
# First, explicitly remove containers by name (handles orphaned containers)
1414
echo "Removing awf containers by name..."
15-
docker rm -f awf-squid awf-agent awf-iptables-init awf-api-proxy awf-cli-proxy 2>/dev/null || true
15+
docker rm -f awf-squid awf-agent awf-iptables-init awf-api-proxy awf-cli-proxy awf-cli-proxy-mcpg 2>/dev/null || true
1616

1717
# Cleanup diagnostic test containers
1818
echo "Stopping docker compose services..."

src/cli.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,9 +1454,8 @@ program
14541454
)
14551455
.option(
14561456
'--cli-proxy-mcpg-image <image>',
1457-
'Docker image for the mcpg DIFC proxy used inside the CLI proxy sidecar\n' +
1458-
' (only used with --build-local; ignored when pulling pre-built GHCR images)\n' +
1459-
' Set by the AWF compiler to control which mcpg version is pulled and run',
1457+
'Docker image for the mcpg DIFC proxy container (runs as a separate service alongside cli-proxy)\n' +
1458+
' Set by the AWF compiler to control which mcpg version is used',
14601459
'ghcr.io/github/gh-aw-mcpg:v0.2.15'
14611460
)
14621461
// -- Logging & Debug --
@@ -2058,6 +2057,7 @@ export async function handlePredownloadAction(options: {
20582057
agentImage: string;
20592058
enableApiProxy: boolean;
20602059
enableCliProxy?: boolean;
2060+
cliProxyMcpgImage?: string;
20612061
}): Promise<void> {
20622062
const { predownloadCommand } = await import('./commands/predownload');
20632063
try {
@@ -2067,6 +2067,7 @@ export async function handlePredownloadAction(options: {
20672067
agentImage: options.agentImage,
20682068
enableApiProxy: options.enableApiProxy,
20692069
enableCliProxy: options.enableCliProxy,
2070+
cliProxyMcpgImage: options.cliProxyMcpgImage,
20702071
});
20712072
} catch (error) {
20722073
const exitCode = (error as Error & { exitCode?: number }).exitCode ?? 1;
@@ -2091,6 +2092,7 @@ program
20912092
)
20922093
.option('--enable-api-proxy', 'Also download the API proxy image', false)
20932094
.option('--enable-cli-proxy', 'Also download the CLI proxy image', false)
2095+
.option('--cli-proxy-mcpg-image <image>', 'Docker image for the mcpg DIFC proxy container', 'ghcr.io/github/gh-aw-mcpg:v0.2.15')
20942096
.action(handlePredownloadAction);
20952097

20962098
// Logs subcommand - view Squid proxy logs

src/commands/predownload.test.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,13 @@ describe('predownload', () => {
4343
]);
4444
});
4545

46-
it('should include cli-proxy when enabled', () => {
46+
it('should include cli-proxy and mcpg when enabled', () => {
4747
const images = resolveImages({ ...defaults, enableCliProxy: true });
4848
expect(images).toEqual([
4949
'ghcr.io/github/gh-aw-firewall/squid:latest',
5050
'ghcr.io/github/gh-aw-firewall/agent:latest',
5151
'ghcr.io/github/gh-aw-firewall/cli-proxy:latest',
52+
'ghcr.io/github/gh-aw-mcpg:v0.2.15',
5253
]);
5354
});
5455

@@ -59,6 +60,17 @@ describe('predownload', () => {
5960
'ghcr.io/github/gh-aw-firewall/agent:latest',
6061
'ghcr.io/github/gh-aw-firewall/api-proxy:latest',
6162
'ghcr.io/github/gh-aw-firewall/cli-proxy:latest',
63+
'ghcr.io/github/gh-aw-mcpg:v0.2.15',
64+
]);
65+
});
66+
67+
it('should use custom mcpg image when specified', () => {
68+
const images = resolveImages({ ...defaults, enableCliProxy: true, cliProxyMcpgImage: 'ghcr.io/github/gh-aw-mcpg:v0.3.0' });
69+
expect(images).toEqual([
70+
'ghcr.io/github/gh-aw-firewall/squid:latest',
71+
'ghcr.io/github/gh-aw-firewall/agent:latest',
72+
'ghcr.io/github/gh-aw-firewall/cli-proxy:latest',
73+
'ghcr.io/github/gh-aw-mcpg:v0.3.0',
6274
]);
6375
});
6476

@@ -135,15 +147,20 @@ describe('predownload', () => {
135147
);
136148
});
137149

138-
it('should pull cli-proxy when enabled', async () => {
150+
it('should pull cli-proxy and mcpg when enabled', async () => {
139151
await predownloadCommand({ ...defaults, enableCliProxy: true });
140152

141-
expect(execa).toHaveBeenCalledTimes(3);
153+
expect(execa).toHaveBeenCalledTimes(4);
142154
expect(execa).toHaveBeenCalledWith(
143155
'docker',
144156
['pull', 'ghcr.io/github/gh-aw-firewall/cli-proxy:latest'],
145157
{ stdio: 'inherit' },
146158
);
159+
expect(execa).toHaveBeenCalledWith(
160+
'docker',
161+
['pull', 'ghcr.io/github/gh-aw-mcpg:v0.2.15'],
162+
{ stdio: 'inherit' },
163+
);
147164
});
148165

149166
it('should throw with exitCode 1 when a pull fails', async () => {

src/commands/predownload.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface PredownloadOptions {
77
agentImage: string;
88
enableApiProxy: boolean;
99
enableCliProxy?: boolean;
10+
cliProxyMcpgImage?: string;
1011
}
1112

1213
/**
@@ -48,9 +49,13 @@ export function resolveImages(options: PredownloadOptions): string[] {
4849
images.push(`${imageRegistry}/api-proxy:${imageTag}`);
4950
}
5051

51-
// Optionally pull cli-proxy
52+
// Optionally pull cli-proxy and its mcpg sidecar
5253
if (options.enableCliProxy) {
5354
images.push(`${imageRegistry}/cli-proxy:${imageTag}`);
55+
// mcpg runs as a separate container; default or user-specified image
56+
const mcpgImage = options.cliProxyMcpgImage || 'ghcr.io/github/gh-aw-mcpg:v0.2.15';
57+
validateImageReference(mcpgImage);
58+
images.push(mcpgImage);
5459
}
5560

5661
return images;

0 commit comments

Comments
 (0)