Skip to content

Commit b551d43

Browse files
Mossakaclaude
andauthored
feat(cli): add --enable-dind flag to opt-in to Docker socket access (#1276)
Add --enable-dind flag that exposes the host Docker socket to the agent container, enabling Docker-in-Docker workflows. By default, the socket remains hidden (mounted as /dev/null) for security. Closes #116 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f67c441 commit b551d43

File tree

4 files changed

+53
-8
lines changed

4 files changed

+53
-8
lines changed

src/cli.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,13 @@ program
10161016
' Example: 3000,8080 or 3000-3010,8000-8090'
10171017
)
10181018

1019+
.option(
1020+
'--enable-dind',
1021+
'Enable Docker-in-Docker by exposing host Docker socket.\n' +
1022+
' WARNING: allows firewall bypass via docker run',
1023+
false
1024+
)
1025+
10191026
// -- API Proxy --
10201027
.option(
10211028
'--enable-api-proxy',
@@ -1343,6 +1350,7 @@ program
13431350
enableHostAccess: options.enableHostAccess,
13441351
allowHostPorts: options.allowHostPorts,
13451352
sslBump: options.sslBump,
1353+
enableDind: options.enableDind,
13461354
allowedUrls,
13471355
enableApiProxy: options.enableApiProxy,
13481356
openaiApiKey: process.env.OPENAI_API_KEY,

src/docker-manager.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,7 @@ describe('docker-manager', () => {
676676
expect(volumes.some((v: string) => v.includes('agent-logs'))).toBe(true);
677677
});
678678

679-
it('should hide Docker socket', () => {
679+
it('should hide Docker socket by default', () => {
680680
const result = generateDockerCompose(mockConfig, mockNetworkConfig);
681681
const agent = result.services.agent;
682682
const volumes = agent.volumes as string[];
@@ -686,6 +686,20 @@ describe('docker-manager', () => {
686686
expect(volumes).toContain('/dev/null:/host/run/docker.sock:ro');
687687
});
688688

689+
it('should expose Docker socket when enableDind is true', () => {
690+
const dindConfig = { ...mockConfig, enableDind: true };
691+
const result = generateDockerCompose(dindConfig, mockNetworkConfig);
692+
const agent = result.services.agent;
693+
const volumes = agent.volumes as string[];
694+
695+
// Docker socket should be mounted read-write, not hidden
696+
expect(volumes).toContain('/var/run/docker.sock:/host/var/run/docker.sock:rw');
697+
expect(volumes).toContain('/run/docker.sock:/host/run/docker.sock:rw');
698+
// Should NOT have /dev/null mounts
699+
expect(volumes).not.toContain('/dev/null:/host/var/run/docker.sock:ro');
700+
expect(volumes).not.toContain('/dev/null:/host/run/docker.sock:ro');
701+
});
702+
689703
it('should mount workspace directory under /host', () => {
690704
const result = generateDockerCompose(mockConfig, mockNetworkConfig);
691705
const agent = result.services.agent;

src/docker-manager.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -720,13 +720,23 @@ export function generateDockerCompose(
720720
fs.writeFileSync(chrootHostsPath, hostsContent, { mode: 0o644 });
721721
agentVolumes.push(`${chrootHostsPath}:/host/etc/hosts:ro`);
722722

723-
// SECURITY: Hide Docker socket to prevent firewall bypass via 'docker run'
724-
// An attacker could otherwise spawn a new container without network restrictions
725-
agentVolumes.push('/dev/null:/host/var/run/docker.sock:ro');
726-
// Also hide /run/docker.sock (symlink on some systems)
727-
agentVolumes.push('/dev/null:/host/run/docker.sock:ro');
728-
729-
logger.debug('Selective mounts configured: system paths (ro), home (rw), Docker socket hidden');
723+
// SECURITY: Docker socket access control
724+
if (config.enableDind) {
725+
logger.warn('Docker-in-Docker enabled: agent can run docker commands (firewall bypass possible)');
726+
// Mount the real Docker socket into the chroot
727+
const dockerSocketPath = '/var/run/docker.sock';
728+
agentVolumes.push(`${dockerSocketPath}:/host${dockerSocketPath}:rw`);
729+
// Also expose the /run/docker.sock symlink if it exists
730+
agentVolumes.push('/run/docker.sock:/host/run/docker.sock:rw');
731+
logger.debug('Selective mounts configured: system paths (ro), home (rw), Docker socket exposed');
732+
} else {
733+
// Hide Docker socket to prevent firewall bypass via 'docker run'
734+
// An attacker could otherwise spawn a new container without network restrictions
735+
agentVolumes.push('/dev/null:/host/var/run/docker.sock:ro');
736+
// Also hide /run/docker.sock (symlink on some systems)
737+
agentVolumes.push('/dev/null:/host/run/docker.sock:ro');
738+
logger.debug('Selective mounts configured: system paths (ro), home (rw), Docker socket hidden');
739+
}
730740

731741
// Add SSL CA certificate mount if SSL Bump is enabled
732742
// This allows the agent container to trust the dynamically-generated CA

src/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,19 @@ export interface WrapperConfig {
386386
*/
387387
sslBump?: boolean;
388388

389+
/**
390+
* Enable Docker-in-Docker by exposing the host Docker socket
391+
*
392+
* When true, the host's Docker socket (/var/run/docker.sock) is mounted
393+
* into the agent container, allowing the agent to run Docker commands.
394+
*
395+
* WARNING: This allows the agent to bypass firewall restrictions by
396+
* spawning new containers without network restrictions.
397+
*
398+
* @default false
399+
*/
400+
enableDind?: boolean;
401+
389402
/**
390403
* URL patterns to allow for HTTPS traffic (requires sslBump: true)
391404
*

0 commit comments

Comments
 (0)