Skip to content

Commit 8d65bc9

Browse files
Copilotlpcox
andauthored
feat: hide /tmp/gh-aw/mcp-logs/ from agent containers (#706)
* Initial plan * feat: hide /tmp/gh-aw/mcp-logs/ directory from agent container Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * test: improve mcp-logs tests to verify mount protection * fix: create /tmp/gh-aw/mcp-logs before Docker mount (#707) * Initial plan * fix: create /tmp/gh-aw/mcp-logs directory before mounting Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * test: add test for /tmp/gh-aw/mcp-logs directory creation Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: use tmpfs to hide /tmp/gh-aw/mcp-logs directory from containers (#709) * Initial plan * fix: revert file approach, need directory solution for mcp-logs Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: use tmpfs to hide /tmp/gh-aw/mcp-logs directory from container Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * test: verify tmpfs mount solution works end-to-end Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: use 0o777 permissions for mcp-logs and squid-logs directories (#710) * Initial plan * fix: set /tmp/gh-aw/mcp-logs to world-writable (0o777) Fixes permission denied error when GitHub Actions workflows try to create subdirectories in /tmp/gh-aw/mcp-logs after AWF runs with sudo. Changes: - Set directory permissions to 0o777 (rwxrwxrwx) instead of 0o755 - Explicitly call chmodSync after mkdirSync to bypass umask - Fix permissions if directory already exists from previous run - Update test to verify 777 permissions Root cause: When AWF runs with sudo (e.g., --enable-chroot), it creates /tmp/gh-aw/mcp-logs owned by root. With 755 permissions, non-root users cannot create subdirectories. Using 777 allows workflows to create subdirectories like /tmp/gh-aw/mcp-logs/safeoutputs without sudo. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: ensure squid logs dir has 777 permissions Apply same fix to squidLogsDir for consistency with mcpLogsDir. Explicitly calls chmodSync to bypass umask effects. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
1 parent 6a967cd commit 8d65bc9

4 files changed

Lines changed: 109 additions & 0 deletions

File tree

examples/basic-curl.sh

100644100755
File mode changed.

src/docker-manager.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,6 +1495,29 @@ describe('docker-manager', () => {
14951495
expect(fs.existsSync(path.join(testDir, 'squid-logs'))).toBe(true);
14961496
});
14971497

1498+
it('should create /tmp/gh-aw/mcp-logs directory with world-writable permissions', async () => {
1499+
const config: WrapperConfig = {
1500+
allowedDomains: ['github.com'],
1501+
agentCommand: 'echo test',
1502+
logLevel: 'info',
1503+
keepContainers: false,
1504+
workDir: testDir,
1505+
};
1506+
1507+
try {
1508+
await writeConfigs(config);
1509+
} catch {
1510+
// May fail, but directory should still be created
1511+
}
1512+
1513+
// Verify /tmp/gh-aw/mcp-logs directory was created
1514+
expect(fs.existsSync('/tmp/gh-aw/mcp-logs')).toBe(true);
1515+
const stats = fs.statSync('/tmp/gh-aw/mcp-logs');
1516+
expect(stats.isDirectory()).toBe(true);
1517+
// Verify permissions are 0o777 (rwxrwxrwx) to allow non-root users to create subdirectories
1518+
expect((stats.mode & 0o777).toString(8)).toBe('777');
1519+
});
1520+
14981521
it('should write squid.conf file', async () => {
14991522
const config: WrapperConfig = {
15001523
allowedDomains: ['github.com', 'example.com'],

src/docker-manager.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,17 @@ export function generateDockerCompose(
718718
dns_search: [], // Disable DNS search domains to prevent embedded DNS fallback
719719
volumes: agentVolumes,
720720
environment,
721+
// Hide /tmp/gh-aw/mcp-logs directory using tmpfs (empty in-memory filesystem)
722+
// This prevents the agent from accessing MCP server logs while still allowing
723+
// the host to write logs to /tmp/gh-aw/mcp-logs/ (e.g., /tmp/gh-aw/mcp-logs/safeoutputs/)
724+
// For normal mode: hide /tmp/gh-aw/mcp-logs
725+
// For chroot mode: hide both /tmp/gh-aw/mcp-logs and /host/tmp/gh-aw/mcp-logs
726+
tmpfs: config.enableChroot
727+
? [
728+
'/tmp/gh-aw/mcp-logs:rw,noexec,nosuid,size=1m',
729+
'/host/tmp/gh-aw/mcp-logs:rw,noexec,nosuid,size=1m',
730+
]
731+
: ['/tmp/gh-aw/mcp-logs:rw,noexec,nosuid,size=1m'],
721732
depends_on: {
722733
'squid-proxy': {
723734
condition: 'service_healthy',
@@ -861,9 +872,28 @@ export async function writeConfigs(config: WrapperConfig): Promise<void> {
861872
const squidLogsDir = config.proxyLogsDir || path.join(config.workDir, 'squid-logs');
862873
if (!fs.existsSync(squidLogsDir)) {
863874
fs.mkdirSync(squidLogsDir, { recursive: true, mode: 0o777 });
875+
// Explicitly set permissions to 0o777 (not affected by umask)
876+
fs.chmodSync(squidLogsDir, 0o777);
864877
}
865878
logger.debug(`Squid logs directory created at: ${squidLogsDir}`);
866879

880+
// Create /tmp/gh-aw/mcp-logs directory
881+
// This directory exists on the HOST for MCP gateway to write logs
882+
// Inside the AWF container, it's hidden via tmpfs mount (see generateDockerCompose)
883+
// Uses mode 0o777 to allow GitHub Actions workflows and MCP gateway to create subdirectories
884+
// even when AWF runs as root (e.g., sudo awf --enable-chroot)
885+
const mcpLogsDir = '/tmp/gh-aw/mcp-logs';
886+
if (!fs.existsSync(mcpLogsDir)) {
887+
fs.mkdirSync(mcpLogsDir, { recursive: true, mode: 0o777 });
888+
// Explicitly set permissions to 0o777 (not affected by umask)
889+
fs.chmodSync(mcpLogsDir, 0o777);
890+
logger.debug(`MCP logs directory created at: ${mcpLogsDir}`);
891+
} else {
892+
// Fix permissions if directory already exists (e.g., created by a previous run)
893+
fs.chmodSync(mcpLogsDir, 0o777);
894+
logger.debug(`MCP logs directory permissions fixed at: ${mcpLogsDir}`);
895+
}
896+
867897
// Use fixed network configuration (network is created by host-iptables.ts)
868898
const networkConfig = {
869899
subnet: '172.30.0.0/24',

tests/integration/credential-hiding.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,4 +294,60 @@ describe('Credential Hiding Security', () => {
294294
expect(output).not.toContain('auth');
295295
}, 120000);
296296
});
297+
298+
describe('MCP Logs Directory Hiding', () => {
299+
test('Test 13: /tmp/gh-aw/mcp-logs/ is hidden in normal mode', async () => {
300+
// Try to access the mcp-logs directory
301+
const result = await runner.runWithSudo(
302+
'ls -la /tmp/gh-aw/mcp-logs/ 2>&1 | grep -v "^\\[" | head -1',
303+
{
304+
allowDomains: ['github.com'],
305+
logLevel: 'debug',
306+
timeout: 60000,
307+
}
308+
);
309+
310+
// With tmpfs mounted over the directory, ls should succeed but show empty directory
311+
// The directory appears to exist (as an empty tmpfs) but contains no files
312+
const allOutput = `${result.stdout}\n${result.stderr}`;
313+
// Verify either:
314+
// 1. Directory listing shows it's effectively empty (total size indicates empty tmpfs)
315+
// 2. Or old /dev/null behavior ("Not a directory")
316+
expect(allOutput).toMatch(/total|Not a directory|cannot access/i);
317+
}, 120000);
318+
319+
test('Test 14: /tmp/gh-aw/mcp-logs/ is hidden in chroot mode', async () => {
320+
// Try to access the mcp-logs directory at /host path
321+
const result = await runner.runWithSudo(
322+
'ls -la /host/tmp/gh-aw/mcp-logs/ 2>&1 | grep -v "^\\[" | head -1',
323+
{
324+
allowDomains: ['github.com'],
325+
logLevel: 'debug',
326+
timeout: 60000,
327+
enableChroot: true,
328+
}
329+
);
330+
331+
// With tmpfs mounted over the directory at /host path, ls should succeed but show empty
332+
const allOutput = `${result.stdout}\n${result.stderr}`;
333+
expect(allOutput).toMatch(/total|Not a directory|cannot access/i);
334+
}, 120000);
335+
336+
test('Test 15: MCP logs files cannot be read in normal mode', async () => {
337+
// Try to read a typical MCP log file path
338+
const result = await runner.runWithSudo(
339+
'cat /tmp/gh-aw/mcp-logs/safeoutputs/log.txt 2>&1 | grep -v "^\\[" | head -1',
340+
{
341+
allowDomains: ['github.com'],
342+
logLevel: 'debug',
343+
timeout: 60000,
344+
}
345+
);
346+
347+
// Should fail with "No such file or directory" (tmpfs is empty)
348+
// This confirms the tmpfs mount is preventing file access to host files
349+
const allOutput = `${result.stdout}\n${result.stderr}`;
350+
expect(allOutput).toMatch(/No such file or directory|Not a directory|cannot access/i);
351+
}, 120000);
352+
});
297353
});

0 commit comments

Comments
 (0)