Skip to content

Commit 1e3f99e

Browse files
lpcoxCopilot
andcommitted
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>
1 parent 92f386c commit 1e3f99e

10 files changed

Lines changed: 265 additions & 149 deletions

File tree

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: 14 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 entrypoint waits for the mcpg TLS cert
6+
# (written to a shared volume at /tmp/proxy-tls), configures gh CLI to
7+
# route through the mcpg container, then starts the Node.js HTTP server.
58
set -e
69

710
echo "[cli-proxy] Starting CLI proxy sidecar..."
811

9-
MCPG_PID=""
1012
NODE_PID=""
1113

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..."
14+
# AWF_MCPG_HOST is set by docker-manager.ts to the mcpg container's IP.
15+
# Fall back to localhost for backward-compatible local testing.
16+
MCPG_HOST="${AWF_MCPG_HOST:-localhost}"
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 ${MCPG_HOST}:${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,24 @@ 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+
export GH_HOST="${MCPG_HOST}:${MCPG_PORT}"
6840
export NODE_EXTRA_CA_CERTS="/tmp/proxy-tls/ca.crt"
6941
export GH_REPO="${GH_REPO:-$GITHUB_REPOSITORY}"
7042

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

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.
45+
# Cleanup handler: stop the Node HTTP server on signal
7646
cleanup() {
7747
echo "[cli-proxy] Shutting down..."
7848
if [ -n "$NODE_PID" ]; then
7949
kill "$NODE_PID" 2>/dev/null || true
8050
wait "$NODE_PID" 2>/dev/null || true
8151
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
8652
}
8753
trap 'cleanup; exit 0' INT TERM
8854

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)