diff --git a/containers/agent/seccomp-profile.json b/containers/agent/seccomp-profile.json new file mode 100644 index 000000000..b6a35e706 --- /dev/null +++ b/containers/agent/seccomp-profile.json @@ -0,0 +1,42 @@ +{ + "defaultAction": "SCMP_ACT_ALLOW", + "architectures": [ + "SCMP_ARCH_X86_64", + "SCMP_ARCH_X86", + "SCMP_ARCH_AARCH64" + ], + "syscalls": [ + { + "names": [ + "kexec_load", + "kexec_file_load", + "reboot", + "init_module", + "finit_module", + "delete_module", + "acct", + "swapon", + "swapoff", + "mount", + "umount", + "umount2", + "pivot_root", + "syslog", + "add_key", + "request_key", + "keyctl", + "uselib", + "personality", + "ustat", + "sysfs", + "vhangup", + "get_kernel_syms", + "query_module", + "create_module", + "nfsservctl" + ], + "action": "SCMP_ACT_ERRNO", + "errnoRet": 1 + } + ] +} diff --git a/docs/logging_quickref.md b/docs/logging_quickref.md index dae72d27e..24a106c66 100644 --- a/docs/logging_quickref.md +++ b/docs/logging_quickref.md @@ -29,6 +29,26 @@ docker exec awf-agent dmesg | grep FW_BLOCKED sudo journalctl -k | grep FW_BLOCKED ``` +### DNS Query Logging (Audit Trail) +```bash +# View all DNS queries made by containers +sudo dmesg | grep FW_DNS_QUERY + +# Using journalctl (systemd) +sudo journalctl -k | grep FW_DNS_QUERY + +# Real-time DNS query monitoring +sudo dmesg -w | grep FW_DNS_QUERY + +# Count DNS queries by destination +sudo dmesg | grep FW_DNS_QUERY | grep -oP 'DST=\K[^ ]+' | sort | uniq -c | sort -rn + +# Show DNS queries to specific resolver (e.g., 8.8.8.8) +sudo dmesg | grep FW_DNS_QUERY | grep 'DST=8.8.8.8' +``` + +**Note:** DNS queries are logged for audit trail purposes. This helps detect potential DNS tunneling attempts or unusual DNS activity. The log prefix `[FW_DNS_QUERY]` is used to identify DNS traffic. + ## Log Format ### Squid Log Entry diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index cd1d0ec00..9ba81e584 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -173,6 +173,29 @@ describe('docker-manager', () => { expect(agent.cap_add).toContain('NET_ADMIN'); }); + it('should apply container hardening measures', () => { + const result = generateDockerCompose(mockConfig, mockNetworkConfig); + const agent = result.services.agent; + + // Verify dropped capabilities for security hardening + expect(agent.cap_drop).toEqual([ + 'NET_RAW', + 'SYS_PTRACE', + 'SYS_MODULE', + 'SYS_RAWIO', + 'MKNOD', + ]); + + // Verify seccomp profile is configured + expect(agent.security_opt).toContain('seccomp=/tmp/awf-test/seccomp-profile.json'); + + // Verify resource limits + expect(agent.mem_limit).toBe('4g'); + expect(agent.memswap_limit).toBe('4g'); + expect(agent.pids_limit).toBe(1000); + expect(agent.cpu_shares).toBe(1024); + }); + it('should disable TTY by default to prevent ANSI escape sequences', () => { const result = generateDockerCompose(mockConfig, mockNetworkConfig); const agent = result.services.agent; diff --git a/src/docker-manager.ts b/src/docker-manager.ts index aed276d94..8ce6eead9 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -261,6 +261,21 @@ export function generateDockerCompose( }, }, cap_add: ['NET_ADMIN'], // Required for iptables + // Drop capabilities to reduce attack surface (security hardening) + cap_drop: [ + 'NET_RAW', // Prevents raw socket creation (iptables bypass attempts) + 'SYS_PTRACE', // Prevents process inspection/debugging (container escape vector) + 'SYS_MODULE', // Prevents kernel module loading + 'SYS_RAWIO', // Prevents raw I/O access + 'MKNOD', // Prevents device node creation + ], + // Apply seccomp profile to restrict dangerous syscalls + security_opt: [`seccomp=${config.workDir}/seccomp-profile.json`], + // Resource limits to prevent DoS attacks (conservative defaults) + mem_limit: '4g', // 4GB memory limit + memswap_limit: '4g', // No swap (same as mem_limit) + pids_limit: 1000, // Max 1000 processes + cpu_shares: 1024, // Default CPU share stdin_open: true, tty: config.tty || false, // Use --tty flag, default to false for clean logs // Escape $ with $$ for Docker Compose variable interpolation @@ -351,6 +366,25 @@ export async function writeConfigs(config: WrapperConfig): Promise { }; logger.debug(`Using network config: ${networkConfig.subnet} (squid: ${networkConfig.squidIp}, agent: ${networkConfig.agentIp})`); + // Copy seccomp profile to work directory for container security + const seccompSourcePath = path.join(__dirname, '..', 'containers', 'agent', 'seccomp-profile.json'); + const seccompDestPath = path.join(config.workDir, 'seccomp-profile.json'); + if (fs.existsSync(seccompSourcePath)) { + fs.copyFileSync(seccompSourcePath, seccompDestPath); + logger.debug(`Seccomp profile written to: ${seccompDestPath}`); + } else { + // If running from dist, try relative to dist + const altSeccompPath = path.join(__dirname, '..', '..', 'containers', 'agent', 'seccomp-profile.json'); + if (fs.existsSync(altSeccompPath)) { + fs.copyFileSync(altSeccompPath, seccompDestPath); + logger.debug(`Seccomp profile written to: ${seccompDestPath}`); + } else { + const message = `Seccomp profile not found at ${seccompSourcePath} or ${altSeccompPath}. Container security hardening requires the seccomp profile.`; + logger.error(message); + throw new Error(message); + } + } + // Write Squid config const squidConfig = generateSquidConfig({ domains: config.allowedDomains, diff --git a/src/host-iptables.test.ts b/src/host-iptables.test.ts index 090b4707b..4d03f620e 100644 --- a/src/host-iptables.test.ts +++ b/src/host-iptables.test.ts @@ -140,6 +140,19 @@ describe('host-iptables', () => { '-j', 'ACCEPT', ]); + // Verify DNS query logging rules (LOG before ACCEPT for audit trail) + expect(mockedExeca).toHaveBeenCalledWith('iptables', [ + '-t', 'filter', '-A', 'FW_WRAPPER', + '-p', 'udp', '-d', '8.8.8.8', '--dport', '53', + '-j', 'LOG', '--log-prefix', '[FW_DNS_QUERY] ', '--log-level', '4', + ]); + + expect(mockedExeca).toHaveBeenCalledWith('iptables', [ + '-t', 'filter', '-A', 'FW_WRAPPER', + '-p', 'tcp', '-d', '8.8.8.8', '--dport', '53', + '-j', 'LOG', '--log-prefix', '[FW_DNS_QUERY] ', '--log-level', '4', + ]); + // Verify DNS rules for trusted servers only expect(mockedExeca).toHaveBeenCalledWith('iptables', [ '-t', 'filter', '-A', 'FW_WRAPPER', @@ -153,6 +166,19 @@ describe('host-iptables', () => { '-j', 'ACCEPT', ]); + // Verify DNS query logging rules for second DNS server + expect(mockedExeca).toHaveBeenCalledWith('iptables', [ + '-t', 'filter', '-A', 'FW_WRAPPER', + '-p', 'udp', '-d', '8.8.4.4', '--dport', '53', + '-j', 'LOG', '--log-prefix', '[FW_DNS_QUERY] ', '--log-level', '4', + ]); + + expect(mockedExeca).toHaveBeenCalledWith('iptables', [ + '-t', 'filter', '-A', 'FW_WRAPPER', + '-p', 'tcp', '-d', '8.8.4.4', '--dport', '53', + '-j', 'LOG', '--log-prefix', '[FW_DNS_QUERY] ', '--log-level', '4', + ]); + expect(mockedExeca).toHaveBeenCalledWith('iptables', [ '-t', 'filter', '-A', 'FW_WRAPPER', '-p', 'udp', '-d', '8.8.4.4', '--dport', '53', @@ -419,6 +445,19 @@ describe('host-iptables', () => { '-j', 'ACCEPT', ]); + // Verify IPv6 DNS query logging rules (LOG before ACCEPT) + expect(mockedExeca).toHaveBeenCalledWith('ip6tables', [ + '-t', 'filter', '-A', 'FW_WRAPPER_V6', + '-p', 'udp', '-d', '2001:4860:4860::8888', '--dport', '53', + '-j', 'LOG', '--log-prefix', '[FW_DNS_QUERY] ', '--log-level', '4', + ]); + + expect(mockedExeca).toHaveBeenCalledWith('ip6tables', [ + '-t', 'filter', '-A', 'FW_WRAPPER_V6', + '-p', 'tcp', '-d', '2001:4860:4860::8888', '--dport', '53', + '-j', 'LOG', '--log-prefix', '[FW_DNS_QUERY] ', '--log-level', '4', + ]); + // Verify IPv6 DNS rule uses ip6tables expect(mockedExeca).toHaveBeenCalledWith('ip6tables', [ '-t', 'filter', '-A', 'FW_WRAPPER_V6', diff --git a/src/host-iptables.ts b/src/host-iptables.ts index fb312233c..1e72214bd 100644 --- a/src/host-iptables.ts +++ b/src/host-iptables.ts @@ -276,12 +276,25 @@ export async function setupHostIptables(squidIp: string, squidPort: number, dnsS // Add IPv4 DNS server rules using iptables for (const dnsServer of ipv4DnsServers) { + // Log DNS queries first (LOG doesn't terminate processing) + await execa('iptables', [ + '-t', 'filter', '-A', CHAIN_NAME, + '-p', 'udp', '-d', dnsServer, '--dport', '53', + '-j', 'LOG', '--log-prefix', '[FW_DNS_QUERY] ', '--log-level', '4', + ]); + await execa('iptables', [ '-t', 'filter', '-A', CHAIN_NAME, '-p', 'udp', '-d', dnsServer, '--dport', '53', '-j', 'ACCEPT', ]); + await execa('iptables', [ + '-t', 'filter', '-A', CHAIN_NAME, + '-p', 'tcp', '-d', dnsServer, '--dport', '53', + '-j', 'LOG', '--log-prefix', '[FW_DNS_QUERY] ', '--log-level', '4', + ]); + await execa('iptables', [ '-t', 'filter', '-A', CHAIN_NAME, '-p', 'tcp', '-d', dnsServer, '--dport', '53', @@ -336,12 +349,25 @@ export async function setupHostIptables(squidIp: string, squidPort: number, dnsS // 4. Allow DNS ONLY to specified trusted IPv6 DNS servers for (const dnsServer of ipv6DnsServers) { + // Log DNS queries first (LOG doesn't terminate processing) + await execa('ip6tables', [ + '-t', 'filter', '-A', CHAIN_NAME_V6, + '-p', 'udp', '-d', dnsServer, '--dport', '53', + '-j', 'LOG', '--log-prefix', '[FW_DNS_QUERY] ', '--log-level', '4', + ]); + await execa('ip6tables', [ '-t', 'filter', '-A', CHAIN_NAME_V6, '-p', 'udp', '-d', dnsServer, '--dport', '53', '-j', 'ACCEPT', ]); + await execa('ip6tables', [ + '-t', 'filter', '-A', CHAIN_NAME_V6, + '-p', 'tcp', '-d', dnsServer, '--dport', '53', + '-j', 'LOG', '--log-prefix', '[FW_DNS_QUERY] ', '--log-level', '4', + ]); + await execa('ip6tables', [ '-t', 'filter', '-A', CHAIN_NAME_V6, '-p', 'tcp', '-d', dnsServer, '--dport', '53', diff --git a/src/types.ts b/src/types.ts index b28925b18..5cc392dfc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -472,14 +472,73 @@ export interface DockerService { /** * Linux capabilities to add to the container - * + * * Grants additional privileges beyond the default container capabilities. * The agent container requires NET_ADMIN for iptables manipulation. - * + * * @example ['NET_ADMIN'] */ cap_add?: string[]; + /** + * Linux capabilities to drop from the container + * + * Removes specific capabilities to reduce attack surface. The firewall drops + * capabilities that could be used for container escape or firewall bypass. + * + * @example ['NET_RAW', 'SYS_PTRACE', 'SYS_MODULE'] + */ + cap_drop?: string[]; + + /** + * Security options for the container + * + * Used for seccomp profiles, AppArmor profiles, and other security configurations. + * + * @example ['seccomp=/path/to/profile.json'] + */ + security_opt?: string[]; + + /** + * Memory limit for the container + * + * Maximum amount of memory the container can use. Prevents DoS attacks + * via memory exhaustion. + * + * @example '4g' + * @example '512m' + */ + mem_limit?: string; + + /** + * Total memory limit including swap + * + * Set equal to mem_limit to disable swap usage. + * + * @example '4g' + */ + memswap_limit?: string; + + /** + * Maximum number of PIDs (processes) in the container + * + * Limits fork bombs and process exhaustion attacks. + * + * @example 1000 + */ + pids_limit?: number; + + /** + * CPU shares (relative weight) + * + * Controls CPU allocation relative to other containers. + * Default is 1024. + * + * @example 1024 + * @example 512 + */ + cpu_shares?: number; + /** * Keep STDIN open even if not attached *