Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/test-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ jobs:
- name: Pre-test cleanup
run: sudo ./scripts/ci/cleanup.sh

- name: Pre-pull GitHub MCP image
run: docker pull ghcr.io/github/github-mcp-server:v0.19.0

- name: Make examples executable
run: chmod +x examples/*.sh

Expand Down Expand Up @@ -66,6 +69,13 @@ jobs:
echo "=== Testing docker-in-docker.sh ==="
sudo ./examples/docker-in-docker.sh

- name: Test github-mcp-smoke.sh
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
echo "=== Testing github-mcp-smoke.sh ==="
sudo ./examples/github-mcp-smoke.sh

# Note: github-copilot.sh is skipped as it requires GITHUB_TOKEN for Copilot CLI
# To test it, you would need to set up a secret with a valid Copilot token

Expand Down
69 changes: 69 additions & 0 deletions .github/workflows/test-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,72 @@ jobs:
/tmp/awf-agent-logs-*/
/tmp/squid-logs-*/
retention-days: 7

test-mcp-github:
name: GitHub MCP Egress Tests
runs-on: ubuntu-latest
timeout-minutes: 12

steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Build project
run: npm run build

- name: Pre-test cleanup
run: sudo ./scripts/ci/cleanup.sh

- name: Pull helper images
run: |
docker pull curlimages/curl:latest
docker pull ghcr.io/github/github-mcp-server:v0.19.0

- name: Run MCP egress tests
id: run-tests
env:
MCP_TEST_FILE: mcp-github.test.ts
GITHUB_TOKEN: ${{ github.token }}
run: |
sudo -E npm run test:integration -- "${MCP_TEST_FILE}" 2>&1 | tee test-output.log
continue-on-error: true

- name: Clean npm cache
if: always()
run: |
sudo npm cache clean --force
sudo rm -rf ~/.npm/_npx

- name: Generate test summary
if: always()
run: |
npx tsx scripts/ci/generate-test-summary.ts "${MCP_TEST_FILE}" "GitHub MCP Egress Tests" test-output.log

- name: Check test results
if: steps.run-tests.outcome == 'failure'
run: exit 1

- name: Post-test cleanup
if: always()
run: sudo ./scripts/ci/cleanup.sh

- name: Upload test logs on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: mcp-github-test-logs
path: |
/tmp/*-test.log
/tmp/awf-*/
/tmp/awf-agent-logs-*/
/tmp/squid-logs-*/
retention-days: 7
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This directory contains example scripts demonstrating common ways to use the Age
| [using-domains-file.sh](using-domains-file.sh) | Using a file to specify allowed domains |
| [blocked-domains.sh](blocked-domains.sh) | Blocking specific domains with allowlist/blocklist |
| [debugging.sh](debugging.sh) | Debug mode with log inspection |
| [github-mcp-smoke.sh](github-mcp-smoke.sh) | GitHub MCP-style traffic allowed while other domains are blocked |
| [domains.txt](domains.txt) | Example domain allowlist file |

## Running Examples
Expand Down
44 changes: 44 additions & 0 deletions examples/github-mcp-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash
# Example: Run a GitHub MCP-style workload through AWF
#
# Uses the GitHub Actions/GITHUB_TOKEN (or a PAT) to call the GitHub API from
# a Dockerized workload, while the firewall blocks non-GitHub domains.
#
# Usage:
# export GITHUB_TOKEN=ghp_xxx # or rely on GitHub Actions default token
# sudo -E ./examples/github-mcp-smoke.sh

set -euo pipefail

if [[ -z "${GITHUB_TOKEN:-}" ]]; then
echo "Error: GITHUB_TOKEN is required (GitHub Actions token or PAT)." >&2
exit 1
fi

echo "=== Pulling helper image (curl) ==="
docker pull curlimages/curl:latest >/dev/null

ALLOW_DOMAINS="api.github.com,github.com,objects.githubusercontent.com,ghcr.io"

echo "=== Calling GitHub API through AWF (allowed) ==="
sudo -E awf \
--allow-domains "${ALLOW_DOMAINS}" \
--log-level warn \
-- 'docker run --rm -e GITHUB_TOKEN curlimages/curl:latest sh -c '"'"'curl -fsS -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/rate_limit'"'"''

echo "=== Starting GitHub MCP Server inside AWF (version check) ==="
sudo -E awf \
--allow-domains "${ALLOW_DOMAINS}" \
--log-level warn \
-- 'timeout 12s docker run --rm -e GITHUB_TOKEN ghcr.io/github/github-mcp-server:v0.19.0 --version'

echo "=== Attempting blocked domain through AWF (should fail) ==="
if sudo -E awf \
--allow-domains "${ALLOW_DOMAINS}" \
--log-level warn \
-- 'docker run --rm curlimages/curl:latest -fsS https://example.com --max-time 8'; then
echo "Unexpected success: example.com should be blocked" >&2
exit 1
fi

echo "=== Example complete: GitHub traffic allowed, other domains blocked ==="
109 changes: 109 additions & 0 deletions tests/integration/mcp-github.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* GitHub MCP-style egress tests
*
* Verifies that AWF preserves GitHub tokens and enforces domain allowlists
* for workloads that mimic the GitHub MCP server (Docker-based, GitHub API).
*/

/// <reference path="../jest-custom-matchers.d.ts" />

import { afterAll, beforeAll, describe, test, expect } from '@jest/globals';
import { createRunner, AwfRunner } from '../fixtures/awf-runner';
import { createDockerHelper, DockerHelper } from '../fixtures/docker-helper';
import { createLogParser } from '../fixtures/log-parser';
import { cleanup } from '../fixtures/cleanup';

const requiredDomains = ['api.github.com', 'github.com', 'objects.githubusercontent.com', 'ghcr.io'];
const githubAuthToken = process.env.GITHUB_TOKEN || process.env.GITHUB_PERSONAL_ACCESS_TOKEN;

const maybeDescribe = githubAuthToken ? describe : describe.skip;

maybeDescribe('GitHub MCP server egress control', () => {
let runner: AwfRunner;
let docker: DockerHelper;

beforeAll(async () => {
await cleanup(false);
runner = createRunner();
docker = createDockerHelper();

// Pre-pull image so the test does not need Docker Hub/ghcr during filtered run
await docker.pullImage('curlimages/curl:latest');
}, 300000);

afterAll(async () => {
await cleanup(false);
}, 30000);

test(
'allows GitHub API access for MCP workload with GitHub token',
async () => {
const result = await runner.runWithSudo(
'docker run --rm -e GITHUB_TOKEN curlimages/curl:latest sh -c \'curl -fsS -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/rate_limit\'',
{
allowDomains: requiredDomains,
logLevel: 'warn',
timeout: 60000,
env: {
...process.env,
GITHUB_TOKEN: githubAuthToken as string,
},
}
);

expect(result).toSucceed();
expect(result.stdout).toContain('"resources"');

if (result.workDir) {
const parser = createLogParser();
const entries = await parser.readSquidLog(result.workDir);
expect(parser.wasAllowed(entries, 'api.github.com')).toBe(true);
}
},
120000
);

test(
'starts GitHub MCP server container inside firewall',
async () => {
const result = await runner.runWithSudo(
'timeout 12s docker run --rm -e GITHUB_TOKEN ghcr.io/github/github-mcp-server:v0.19.0 --version',
{
allowDomains: requiredDomains,
logLevel: 'warn',
timeout: 60000,
env: {
...process.env,
GITHUB_TOKEN: githubAuthToken as string,
},
}
);

expect(result).toSucceed();
},
120000
);

test(
'blocks non-GitHub domains for MCP workload',
async () => {
const result = await runner.runWithSudo(
'docker run --rm curlimages/curl:latest -fsS https://example.com --max-time 8',
{
allowDomains: requiredDomains,
logLevel: 'warn',
timeout: 60000,
}
);

expect(result).toFail();

if (result.workDir) {
const parser = createLogParser();
const entries = await parser.readSquidLog(result.workDir);
expect(parser.wasBlocked(entries, 'example.com')).toBe(true);
}
},
120000
);
});
Loading