Skip to content

Commit 5d61af1

Browse files
Mossakaclaude
andauthored
feat: add --enable-host-access flag and fix CONNECT to port 80 (#190)
This PR addresses issue #189 by: 1. Making host.docker.internal opt-in via --enable-host-access flag - By default, containers cannot resolve host.docker.internal - When enabled, adds extra_hosts to both Squid and agent containers - Shows security warning when combined with host.docker.internal domain 2. Allowing CONNECT method to Safe_ports (80 and 443) - Changes `http_access deny CONNECT !SSL_ports` to `!Safe_ports` - Required because Node.js fetch uses CONNECT for HTTP through proxy - Domain ACLs remain the primary security control Security considerations: - Host access is opt-in to prevent accidental exposure of host services - Warning displayed when host.docker.internal is in allowed domains - Safe_ports change has minimal security impact as domain filtering is primary control Closes #189 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c6ab07a commit 5d61af1

7 files changed

Lines changed: 165 additions & 4 deletions

File tree

docs/usage.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,19 @@ sudo awf [options] <command>
88
Options:
99
--allow-domains <domains> Comma-separated list of allowed domains (required)
1010
Example: github.com,api.github.com,arxiv.org
11+
--allow-domains-file <path> Path to file containing allowed domains
12+
--block-domains <domains> Comma-separated list of blocked domains
13+
--block-domains-file <path> Path to file containing blocked domains
14+
--enable-host-access Enable access to host services via host.docker.internal
15+
(see "Host Access" section for security implications)
1116
--log-level <level> Log level: debug, info, warn, error (default: info)
1217
--keep-containers Keep containers running after command exits
1318
--work-dir <dir> Working directory for temporary files
19+
--dns-servers <servers> Comma-separated list of DNS servers (default: 8.8.8.8,8.8.4.4)
20+
-e, --env <KEY=VALUE> Additional environment variables (can repeat)
21+
--env-all Pass all host environment variables to container
22+
-v, --mount <path:path> Volume mount (host_path:container_path[:ro|rw])
23+
--tty Allocate a pseudo-TTY for interactive tools
1424
-V, --version Output the version number
1525
-h, --help Display help for command
1626
@@ -254,6 +264,48 @@ sudo awf \
254264
- Block known bad domains while allowing a curated list
255265
- Prevent access to internal services from AI agents
256266

267+
## Host Access (MCP Gateways)
268+
269+
When running MCP gateways or other services on your host machine that need to be accessible from inside the firewall, use the `--enable-host-access` flag.
270+
271+
### Enabling Host Access
272+
273+
```bash
274+
# Enable access to services running on the host via host.docker.internal
275+
sudo awf \
276+
--enable-host-access \
277+
--allow-domains host.docker.internal \
278+
-- curl http://host.docker.internal:8080
279+
```
280+
281+
### Security Considerations
282+
283+
> ⚠️ **Security Warning**: When `--enable-host-access` is combined with `host.docker.internal` in `--allow-domains`, containers can access **ANY service** running on the host machine, including:
284+
> - Local databases (PostgreSQL, MySQL, Redis)
285+
> - Development servers
286+
> - Other sensitive services
287+
>
288+
> Only enable this for trusted workloads like MCP gateways.
289+
290+
**Why opt-in?** By default, `host.docker.internal` hostname resolution is disabled to prevent containers from accessing host services. This is a defense-in-depth measure against malicious code attempting to access local resources.
291+
292+
### Example: MCP Gateway on Host
293+
294+
```bash
295+
# Start your MCP gateway on the host (port 8080)
296+
./my-mcp-gateway --port 8080 &
297+
298+
# Run awf with host access enabled
299+
sudo awf \
300+
--enable-host-access \
301+
--allow-domains host.docker.internal,api.github.com \
302+
-- 'copilot --mcp-gateway http://host.docker.internal:8080 --prompt "test"'
303+
```
304+
305+
### CONNECT Method on Port 80
306+
307+
The firewall allows the HTTP CONNECT method on both ports 80 and 443. This is required because some HTTP clients (e.g., Node.js fetch) use the CONNECT method even for HTTP connections when going through a proxy. Domain ACLs remain the primary security control.
308+
257309
## Limitations
258310

259311
### No Internationalized Domains

src/cli.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,13 @@ program
384384
'--proxy-logs-dir <path>',
385385
'Directory to save Squid proxy logs to (writes access.log directly to this directory)'
386386
)
387+
.option(
388+
'--enable-host-access',
389+
'Enable access to host services via host.docker.internal. ' +
390+
'Security warning: When combined with --allow-domains host.docker.internal, ' +
391+
'containers can access ANY service on the host machine.',
392+
false
393+
)
387394
.argument('[args...]', 'Command and arguments to execute (use -- to separate from options)')
388395
.action(async (args: string[], options) => {
389396
// Require -- separator for passing command arguments
@@ -549,6 +556,7 @@ program
549556
containerWorkDir: options.containerWorkdir,
550557
dnsServers,
551558
proxyLogsDir: options.proxyLogsDir,
559+
enableHostAccess: options.enableHostAccess,
552560
};
553561

554562
// Warn if --env-all is used
@@ -557,6 +565,18 @@ program
557565
logger.warn(' This may expose sensitive credentials if logs or configs are shared');
558566
}
559567

568+
// Warn if --enable-host-access is used with host.docker.internal in allowed domains
569+
if (config.enableHostAccess) {
570+
const hasHostDomain = allowedDomains.some(d =>
571+
d === 'host.docker.internal' || d.endsWith('.host.docker.internal')
572+
);
573+
if (hasHostDomain) {
574+
logger.warn('⚠️ Host access enabled with host.docker.internal in allowed domains');
575+
logger.warn(' Containers can access ANY service running on the host machine');
576+
logger.warn(' Only use this for trusted workloads (e.g., MCP gateways)');
577+
}
578+
}
579+
560580
// Log config with redacted secrets
561581
const redactedConfig = {
562582
...config,

src/docker-manager.test.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,44 @@ describe('docker-manager', () => {
317317
expect(agent.dns_search).toEqual([]);
318318
});
319319

320-
it('should configure extra_hosts for host.docker.internal', () => {
320+
it('should NOT configure extra_hosts by default (opt-in for security)', () => {
321321
const result = generateDockerCompose(mockConfig, mockNetworkConfig);
322322
const agent = result.services.agent;
323+
const squid = result.services['squid-proxy'];
324+
325+
expect(agent.extra_hosts).toBeUndefined();
326+
expect(squid.extra_hosts).toBeUndefined();
327+
});
328+
329+
describe('enableHostAccess option', () => {
330+
it('should configure extra_hosts when enableHostAccess is true', () => {
331+
const config = { ...mockConfig, enableHostAccess: true };
332+
const result = generateDockerCompose(config, mockNetworkConfig);
333+
const agent = result.services.agent;
334+
const squid = result.services['squid-proxy'];
335+
336+
expect(agent.extra_hosts).toEqual(['host.docker.internal:host-gateway']);
337+
expect(squid.extra_hosts).toEqual(['host.docker.internal:host-gateway']);
338+
});
339+
340+
it('should NOT configure extra_hosts when enableHostAccess is false', () => {
341+
const config = { ...mockConfig, enableHostAccess: false };
342+
const result = generateDockerCompose(config, mockNetworkConfig);
343+
const agent = result.services.agent;
344+
const squid = result.services['squid-proxy'];
345+
346+
expect(agent.extra_hosts).toBeUndefined();
347+
expect(squid.extra_hosts).toBeUndefined();
348+
});
323349

324-
expect(agent.extra_hosts).toEqual(['host.docker.internal:host-gateway']);
350+
it('should NOT configure extra_hosts when enableHostAccess is undefined', () => {
351+
const result = generateDockerCompose(mockConfig, mockNetworkConfig);
352+
const agent = result.services.agent;
353+
const squid = result.services['squid-proxy'];
354+
355+
expect(agent.extra_hosts).toBeUndefined();
356+
expect(squid.extra_hosts).toBeUndefined();
357+
});
325358
});
326359

327360
it('should override environment variables with additionalEnv', () => {

src/docker-manager.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,15 @@ export function generateDockerCompose(
186186
ports: [`${SQUID_PORT}:${SQUID_PORT}`],
187187
};
188188

189+
// Only enable host.docker.internal when explicitly requested via --enable-host-access
190+
// This allows containers to reach services on the host machine (e.g., MCP gateways)
191+
// Security note: When combined with allowing host.docker.internal domain,
192+
// containers can access any port on the host
193+
if (config.enableHostAccess) {
194+
squidService.extra_hosts = ['host.docker.internal:host-gateway'];
195+
logger.debug('Host access enabled: host.docker.internal will resolve to host gateway');
196+
}
197+
189198
// Use GHCR image or build locally
190199
if (useGHCR) {
191200
squidService.image = `${registry}/squid:${tag}`;
@@ -297,7 +306,6 @@ export function generateDockerCompose(
297306
},
298307
dns: dnsServers, // Use configured DNS servers (prevents DNS exfiltration)
299308
dns_search: [], // Disable DNS search domains to prevent embedded DNS fallback
300-
extra_hosts: ['host.docker.internal:host-gateway'], // Enable host.docker.internal on Linux
301309
volumes: agentVolumes,
302310
environment,
303311
depends_on: {
@@ -337,6 +345,11 @@ export function generateDockerCompose(
337345
logger.debug(`Set container working directory to: ${config.containerWorkDir}`);
338346
}
339347

348+
// Enable host.docker.internal for agent when --enable-host-access is set
349+
if (config.enableHostAccess) {
350+
agentService.extra_hosts = ['host.docker.internal:host-gateway'];
351+
}
352+
340353
// Use GHCR image or build locally
341354
if (useGHCR) {
342355
agentService.image = `${registry}/agent:${tag}`;

src/squid-config.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,24 @@ describe('generateSquidConfig', () => {
332332
expect(result).toContain('logformat firewall_detailed');
333333
});
334334

335+
it('should allow CONNECT to Safe_ports (80 and 443) for HTTP proxy compatibility', () => {
336+
// See: https://github.com/githubnext/gh-aw-firewall/issues/189
337+
// Node.js fetch uses CONNECT method even for HTTP connections when proxied
338+
const config: SquidConfig = {
339+
domains: ['example.com'],
340+
port: defaultPort,
341+
};
342+
const result = generateSquidConfig(config);
343+
344+
// Should deny CONNECT to non-Safe_ports (not just SSL_ports)
345+
expect(result).toContain('http_access deny CONNECT !Safe_ports');
346+
// Should NOT deny CONNECT to non-SSL_ports (would block port 80)
347+
expect(result).not.toContain('http_access deny CONNECT !SSL_ports');
348+
// Safe_ports should include both 80 and 443
349+
expect(result).toContain('acl Safe_ports port 80');
350+
expect(result).toContain('acl Safe_ports port 443');
351+
});
352+
335353
it('should deny access to domains not in the allowlist', () => {
336354
const config: SquidConfig = {
337355
domains: ['example.com'],

src/squid-config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,11 @@ acl CONNECT method CONNECT
309309
# Access rules
310310
# Deny unsafe ports first
311311
http_access deny !Safe_ports
312-
http_access deny CONNECT !SSL_ports
312+
# Allow CONNECT to Safe_ports (80 and 443) instead of just SSL_ports (443)
313+
# This is required because some HTTP clients (e.g., Node.js fetch) use CONNECT
314+
# method even for HTTP connections when going through a proxy.
315+
# See: gh-aw-firewall issue #189
316+
http_access deny CONNECT !Safe_ports
313317
314318
${accessRulesSection}# Deny requests to unknown domains (not in allow-list)
315319
# This applies to all sources including localnet

src/types.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,27 @@ export interface WrapperConfig {
233233
* @example '/tmp/my-proxy-logs'
234234
*/
235235
proxyLogsDir?: string;
236+
237+
/**
238+
* Enable access to host services via host.docker.internal
239+
*
240+
* When true, adds `host.docker.internal` hostname resolution to containers,
241+
* allowing traffic to reach services running on the host machine.
242+
*
243+
* **Security Warning**: When enabled and `host.docker.internal` is added to
244+
* --allow-domains, containers can access ANY service running on the host,
245+
* including databases, APIs, and other sensitive services. Only enable this
246+
* when you specifically need container-to-host communication (e.g., for MCP
247+
* gateways running on the host).
248+
*
249+
* @default false
250+
* @example
251+
* ```bash
252+
* # Enable host access for MCP gateway on host
253+
* awf --enable-host-access --allow-domains host.docker.internal -- curl http://host.docker.internal:8080
254+
* ```
255+
*/
256+
enableHostAccess?: boolean;
236257
}
237258

238259
/**

0 commit comments

Comments
 (0)