Skip to content

Commit 13244a6

Browse files
committed
fix: narrow home directory mounts to prevent access to sensitive paths
SECURITY: Replace broad $HOME:$HOME mount with specific narrow mounts: - Mount only ~/.copilot (ro) for MCP config instead of entire home - Mount only the workspace directory (containerWorkDir) for project files - Mount ~/.cargo (ro) and ~/.local/bin (ro) only if they exist (chroot mode) This prevents the agent from accessing sensitive paths like: - ~/actions-runner (GitHub Actions runner config and credentials) - ~/work (other repositories being checked out) - Other sensitive dotfiles and directories The agent still has access to: - ~/.copilot/mcp-config.json (read-only) for MCP server configuration - ~/.copilot/logs (read-write via overlay) for debug logging - The specific workspace directory being worked on (read-write) - /tmp for temporary files
1 parent 8ee3797 commit 13244a6

2 files changed

Lines changed: 58 additions & 13 deletions

File tree

src/docker-manager.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -580,18 +580,22 @@ describe('docker-manager', () => {
580580
expect(volumes).toContain('/dev/null:/host/run/docker.sock:ro');
581581
});
582582

583-
it('should mount user home directory under /host when enableChroot is true', () => {
583+
it('should mount specific home subdirectories under /host when enableChroot is true', () => {
584584
const configWithChroot = {
585585
...mockConfig,
586-
enableChroot: true
586+
enableChroot: true,
587+
containerWorkDir: '/home/runner/work/repo/repo'
587588
};
588589
const result = generateDockerCompose(configWithChroot, mockNetworkConfig);
589590
const agent = result.services.agent;
590591
const volumes = agent.volumes as string[];
591592

592-
// Should mount home directory under /host for chroot access (read-write)
593+
// Should mount workspace directory under /host for chroot access (read-write)
594+
expect(volumes).toContain('/home/runner/work/repo/repo:/host/home/runner/work/repo/repo:rw');
595+
596+
// Should NOT mount entire home directory (security: prevents access to ~/actions-runner, etc.)
593597
const homeDir = process.env.HOME || '/root';
594-
expect(volumes).toContain(`${homeDir}:/host${homeDir}:rw`);
598+
expect(volumes).not.toContain(`${homeDir}:/host${homeDir}:rw`);
595599
});
596600

597601
it('should add SYS_CHROOT and SYS_ADMIN capabilities when enableChroot is true', () => {

src/docker-manager.ts

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -429,16 +429,35 @@ export function generateDockerCompose(
429429
// Note: UID/GID values are logged by the container entrypoint if needed for debugging
430430

431431
// Build volumes list for agent execution container
432-
// For chroot mode, use the real user's home (not /root when running with sudo)
432+
// SECURITY: Mount only specific paths needed, NOT the entire home directory
433+
// This prevents access to sensitive paths like ~/actions-runner, ~/work (other repos), etc.
433434
const effectiveHome = config.enableChroot ? getRealUserHome() : (process.env.HOME || '/root');
434435
const agentVolumes: string[] = [
435436
// Essential mounts that are always included
436437
'/tmp:/tmp:rw',
437-
`${effectiveHome}:${effectiveHome}:rw`,
438-
// Mount agent logs directory to workDir for persistence
439-
`${config.workDir}/agent-logs:${effectiveHome}/.copilot/logs:rw`,
440438
];
441439

440+
// Mount specific subdirectories of home instead of the entire home directory
441+
// This prevents access to sensitive paths like ~/actions-runner, ~/work (other repos)
442+
const copilotConfigDir = path.join(effectiveHome, '.copilot');
443+
444+
// Ensure .copilot directory exists on host before mounting
445+
if (!fs.existsSync(copilotConfigDir)) {
446+
fs.mkdirSync(copilotConfigDir, { recursive: true });
447+
}
448+
449+
// Mount ~/.copilot for MCP config (read-only) and logs (write via separate mount)
450+
agentVolumes.push(`${copilotConfigDir}:${copilotConfigDir}:ro`);
451+
// Mount agent logs directory to workDir for persistence (overlays the ro mount above)
452+
agentVolumes.push(`${config.workDir}/agent-logs:${effectiveHome}/.copilot/logs:rw`);
453+
454+
// Mount the workspace directory if specified (the actual project being worked on)
455+
if (config.containerWorkDir && config.containerWorkDir !== '/workspace') {
456+
// Only mount if it's a real path (not the default /workspace)
457+
agentVolumes.push(`${config.containerWorkDir}:${config.containerWorkDir}:rw`);
458+
logger.debug(`Mounting workspace directory: ${config.containerWorkDir}`);
459+
}
460+
442461
// Add chroot-related volume mounts when --enable-chroot is specified
443462
// These mounts enable chroot /host to work properly for running host binaries
444463
if (config.enableChroot) {
@@ -472,11 +491,33 @@ export function generateDockerCompose(
472491
'/dev:/host/dev:ro', // Read-only device nodes (needed by some runtimes)
473492
);
474493

475-
// User home directory for project files and Rust/Cargo (read-write)
476-
// Note: $HOME is already mounted at the container level, this adds it under /host
477-
// Use getRealUserHome() to get the actual user's home (not /root when running with sudo)
494+
// SECURITY: Mount specific home subdirectories instead of entire $HOME
495+
// This prevents access to sensitive paths like ~/actions-runner, ~/work (other repos)
478496
const userHome = getRealUserHome();
479-
agentVolumes.push(`${userHome}:/host${userHome}:rw`);
497+
498+
// Mount ~/.copilot for MCP config under /host (read-only for chroot)
499+
const hostCopilotDir = path.join(userHome, '.copilot');
500+
if (fs.existsSync(hostCopilotDir)) {
501+
agentVolumes.push(`${hostCopilotDir}:/host${hostCopilotDir}:ro`);
502+
}
503+
504+
// Mount ~/.cargo for Rust binaries (read-only) if it exists
505+
const hostCargoDir = path.join(userHome, '.cargo');
506+
if (fs.existsSync(hostCargoDir)) {
507+
agentVolumes.push(`${hostCargoDir}:/host${hostCargoDir}:ro`);
508+
}
509+
510+
// Mount ~/.local/bin for user-installed tools (read-only) if it exists
511+
const hostLocalBin = path.join(userHome, '.local', 'bin');
512+
if (fs.existsSync(hostLocalBin)) {
513+
agentVolumes.push(`${hostLocalBin}:/host${hostLocalBin}:ro`);
514+
}
515+
516+
// Mount the workspace directory under /host if specified
517+
if (config.containerWorkDir && config.containerWorkDir !== '/workspace') {
518+
agentVolumes.push(`${config.containerWorkDir}:/host${config.containerWorkDir}:rw`);
519+
logger.debug(`Mounting workspace directory under /host: ${config.containerWorkDir}`);
520+
}
480521

481522
// /tmp is needed for chroot mode to write:
482523
// - Temporary command scripts: /host/tmp/awf-cmd-$$.sh
@@ -533,7 +574,7 @@ export function generateDockerCompose(
533574
// Also hide /run/docker.sock (symlink on some systems)
534575
agentVolumes.push('/dev/null:/host/run/docker.sock:ro');
535576

536-
logger.debug('Selective mounts configured: system paths (ro), home (rw), Docker socket hidden');
577+
logger.debug('Selective mounts configured: system paths (ro), workspace (rw), Docker socket hidden');
537578
}
538579

539580
// Add SSL CA certificate mount if SSL Bump is enabled

0 commit comments

Comments
 (0)