-
Notifications
You must be signed in to change notification settings - Fork 6
Lpcox/remove home runner access #641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
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
SECURITY: The docker-compose.yml file contains sensitive environment variables (GITHUB_TOKEN, etc.) and was accessible via the /tmp volume mount. Now deleted immediately after docker compose up succeeds, since Docker only needs the file during startup. - Added fs.unlinkSync call after successful container startup - Added test verifying compose file is deleted - Logs debug message on successful deletion
|
Chroot tests failed Smoke Chroot failed - See logs for details. |
|
💫 TO BE CONTINUED... Smoke Claude failed! Our hero faces unexpected challenges... |
|
📰 DEVELOPING STORY: Smoke Copilot reports failed. Our correspondents are investigating the incident... |
|
🌑 The shadows whisper... Smoke Codex failed. The oracle requires further meditation... |
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (1 files)
Coverage comparison generated by |
|
@Mossaka This PR is meant to address these issues (among others) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR aims to harden the Docker execution environment by reducing host filesystem exposure (notably the runner’s home directory) and by removing sensitive artifacts after container startup.
Changes:
- Replaces full
$HOMEbind-mounts with selective mounts (e.g.,~/.copilot, agent logs, optional workspace) in both normal and chroot modes. - Adds post-start deletion of
docker-compose.ymlto reduce the lifetime of on-disk secrets. - Updates unit tests to reflect the new mount behavior and to assert compose-file deletion.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
src/docker-manager.ts |
Implements selective home/workspace mounts and deletes docker-compose.yml after startup. |
src/docker-manager.test.ts |
Updates chroot mount expectations and adds a test for compose-file deletion. |
Comments suppressed due to low confidence (1)
src/docker-manager.ts:520
- Same issue as the non-chroot case: mounting
${config.containerWorkDir}under/hostassumescontainerWorkDiris a host path. In chroot mode this is especially likely to break whencontainerWorkDiris something like/workspace/...(container path) rather than/home/runner/...(host path). Consider separating “container working dir” from “host workspace path to mount into /host”.
// Mount the workspace directory under /host if specified
if (config.containerWorkDir && config.containerWorkDir !== '/workspace') {
agentVolumes.push(`${config.containerWorkDir}:/host${config.containerWorkDir}:rw`);
logger.debug(`Mounting workspace directory under /host: ${config.containerWorkDir}`);
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // SECURITY: Mount only specific paths needed, NOT the entire home directory | ||
| // This prevents access to sensitive paths like ~/actions-runner, ~/work (other repos), etc. | ||
| const effectiveHome = config.enableChroot ? getRealUserHome() : (process.env.HOME || '/root'); | ||
| const agentVolumes: string[] = [ | ||
| // Essential mounts that are always included | ||
| '/tmp:/tmp:rw', | ||
| `${effectiveHome}:${effectiveHome}:rw`, | ||
| // Mount agent logs directory to workDir for persistence | ||
| `${config.workDir}/agent-logs:${effectiveHome}/.copilot/logs:rw`, | ||
| ]; |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new comment says we mount only specific paths and not the entire home directory, but in the non-chroot default path the code still prepends the blanket /:/host:rw mount (later in this function), which exposes the full home directory under /host/.... Either tighten the default mounts to actually enforce this security property, or adjust the comment to avoid overstating the protection provided in the default (non-chroot, no custom mounts) mode.
See below for a potential fix:
// SECURITY: Restrict direct bind mounts from the home directory to specific paths
// NOTE: In non-chroot mode, a broader host filesystem mount (e.g. /:/host:rw) may still exist
const effectiveHome = config.enableChroot ? getRealUserHome() : (process.env.HOME || '/root');
const agentVolumes: string[] = [
// Essential mounts that are always included
'/tmp:/tmp:rw',
];
// Directly mount only specific subdirectories of the home directory, not the entire home path
// This reduces direct exposure of paths like ~/actions-runner or other repositories via bind mounts
|
|
||
| // Ensure .copilot directory exists on host before mounting | ||
| if (!fs.existsSync(copilotConfigDir)) { | ||
| fs.mkdirSync(copilotConfigDir, { recursive: true }); |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
generateDockerCompose() now creates ~/.copilot on the host unconditionally. This introduces a side effect and can throw (e.g., if $HOME resolves to an unwritable path), which would prevent config generation. Consider avoiding host filesystem mutation here (let the caller ensure the directory exists), or wrap mkdirSync in a try/catch and emit a clear error with the path that failed.
| fs.mkdirSync(copilotConfigDir, { recursive: true }); | |
| try { | |
| fs.mkdirSync(copilotConfigDir, { recursive: true }); | |
| } catch (err) { | |
| const message = `Failed to create Copilot config directory on host: ${copilotConfigDir}. ${err instanceof Error ? err.message : String(err)}`; | |
| logger.error(message); | |
| throw new Error(message); | |
| } |
| // Only mount if it's a real path (not the default /workspace) | ||
| agentVolumes.push(`${config.containerWorkDir}:${config.containerWorkDir}:rw`); | ||
| logger.debug(`Mounting workspace directory: ${config.containerWorkDir}`); |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
containerWorkDir is defined/used as a container working directory, but this change also treats it as a host bind-mount source (${config.containerWorkDir}:${config.containerWorkDir}). If containerWorkDir is set to a container-only path (e.g. /workspace/project, which is already covered by existing tests), docker compose up will fail because the host path likely doesn't exist. Consider introducing a separate hostWorkDir/workspaceMount option, or only adding this bind mount when the host path exists (and otherwise rely on volumeMounts).
This issue also appears on line 516 of the same file.
| // Only mount if it's a real path (not the default /workspace) | |
| agentVolumes.push(`${config.containerWorkDir}:${config.containerWorkDir}:rw`); | |
| logger.debug(`Mounting workspace directory: ${config.containerWorkDir}`); | |
| // Only mount if it's a real host path (not the default /workspace or a container-only path) | |
| if (fs.existsSync(config.containerWorkDir)) { | |
| agentVolumes.push(`${config.containerWorkDir}:${config.containerWorkDir}:rw`); | |
| logger.debug(`Mounting workspace directory: ${config.containerWorkDir}`); | |
| } else { | |
| logger.debug( | |
| `Not mounting workspace directory '${config.containerWorkDir}' because it does not exist on the host; assuming container-only path.`, | |
| ); | |
| } |
|
|
||
| // SECURITY: Immediately delete docker-compose.yml after containers start | ||
| // This file contains sensitive environment variables (tokens, secrets) that | ||
| // would otherwise be readable by the agent via the /tmp mount until cleanup. | ||
| // Docker Compose only needs the file at startup, not during execution. | ||
| const composeFile = path.join(workDir, 'docker-compose.yml'); | ||
| try { | ||
| if (fs.existsSync(composeFile)) { | ||
| fs.unlinkSync(composeFile); | ||
| logger.debug('Deleted docker-compose.yml (contained sensitive environment variables)'); | ||
| } | ||
| } catch (err) { | ||
| logger.debug('Could not delete docker-compose.yml:', err); | ||
| } |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deleting docker-compose.yml immediately after docker compose up will break later cleanup: stopContainers() currently runs docker compose down -v with cwd: workDir, which requires the compose file to still exist. It also conflicts with --keep-containers, which currently promises to preserve config files. If you want to eliminate secrets from disk, consider switching cleanup to container-name based teardown (docker rm -f awf-squid awf-agent, plus any needed network/volume cleanup), or keep a compose file outside the /tmp bind mount and pass it via docker compose -f <path> for both up/down.
| // SECURITY: Immediately delete docker-compose.yml after containers start | |
| // This file contains sensitive environment variables (tokens, secrets) that | |
| // would otherwise be readable by the agent via the /tmp mount until cleanup. | |
| // Docker Compose only needs the file at startup, not during execution. | |
| const composeFile = path.join(workDir, 'docker-compose.yml'); | |
| try { | |
| if (fs.existsSync(composeFile)) { | |
| fs.unlinkSync(composeFile); | |
| logger.debug('Deleted docker-compose.yml (contained sensitive environment variables)'); | |
| } | |
| } catch (err) { | |
| logger.debug('Could not delete docker-compose.yml:', err); | |
| } |
| it('should delete docker-compose.yml after containers start (security: removes sensitive env vars)', async () => { | ||
| // Create a docker-compose.yml file with sensitive data | ||
| const composeFile = path.join(testDir, 'docker-compose.yml'); | ||
| fs.writeFileSync(composeFile, 'environment:\n GITHUB_TOKEN: secret123\n'); | ||
|
|
||
| mockExecaFn.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 } as any); | ||
| mockExecaFn.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 } as any); | ||
|
|
||
| await startContainers(testDir, ['github.com']); | ||
|
|
||
| // docker-compose.yml should be deleted after containers start | ||
| expect(fs.existsSync(composeFile)).toBe(false); | ||
| }); |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new test verifies deletion of docker-compose.yml after startup, but there is no corresponding coverage ensuring teardown still works. Given stopContainers() uses docker compose down (which requires the compose file), please add a test that exercises the full start→stop flow (or update the stop logic) so we don’t regress cleanup behavior.
No description provided.