cd /news/ai-safety/building-scamshield-ai-week-2-docker… · home › topics › ai-safety › article
[ARTICLE · art-163341] src=dev.to ↗ pub= topic=ai-safety verified=true sentiment=↑ positive

Building ScamShield AI: Week 2 — Docker Security, Firewall Bypasses, and Fail-Closed Architecture

A developer building ScamShield AI, an evidence-grounded threat intelligence platform for analyzing Indian scam SMS and WhatsApp messages, hardened the platform's Mitmproxy-based proxy container by reordering iptables rules so private-range and cloud-metadata drops are enforced before stateful connection acceptance, closing an SSRF bypass against endpoints like 169.254.169.254. The developer also rewrote the container entrypoint to fail closed, aborting startup with exit code 1 if iptables or NET_ADMIN is unavailable or any rule fails verification, and fixed a boot crash caused by mitmproxy 10.4.2 deprecating the --termlog-verbosity flag in favor of --set termlog_verbosity=info.

by read5 min views1 publishedNov 3, 2026

If you caught Week 1, you know I'm building ScamShield AI — an evidence-grounded threat intelligence platform designed to safely analyze Indian scam SMS messages and WhatsApp forwards (fake KYC alerts, electricity cut-off threats, UPI traps).

To safely "click" a scammer's link without letting them compromise our backend infrastructure, we are building an isolated Docker sandbox using Playwright (Headless Chromium) and Mitmproxy (The Network Guard).

This week was entirely about hardening Stage 1: The Proxy Container.

I learned the hard way that building a Docker container is trivial, but securing it against a determined attacker probing from the inside is a completely different engineering challenge.

Here are my biggest technical takeaways, bugs, and architectural wins from Week 2.

iptables Rule Ordering is a Strict Security Boundary In iptables (the Linux kernel packet filter), rules are evaluated sequentially from top to bottom. The first match wins.

I originally wrote my firewall rules in this intuitive order:

1. Allow loopback (lo)
2. Allow ESTABLISHED, RELATED traffic
3. DROP private IPs (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.169.254)

An attacker exploiting a network connection could potentially match an existing state or craft packets that trigger state-tracking rules prematurely. Because the ESTABLISHED, RELATED rule was placed at #2, any connection matching that condition is accepted immediately. It never reaches the DROP rules sitting at #3.

On a threat-intelligence scanner inspecting hostile URLs, this is a dangerous bypass that risks Server-Side Request Forgery (SSRF) against cloud metadata endpoints (169.254.169.254).

We refactored the firewall initialization into 6 strict phases. All prohibited destination drops are enforced in Phase 1, before stateful connection acceptance is even evaluated.

Phase 1: DROP 169.254.169.254/32 (AWS/Cloud Metadata)
         DROP 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (RFC1918 Private Ranges)
         DROP 100.64.0.0/10 (CGNAT)
...
Phase 4: ACCEPT ESTABLISHED, RELATED
Phase 5: DROP ALL (Default Policy Fallback)

If a packet targets an internal address, the kernel drops it immediately on line 1. No connection tracking rule can resurrect it.

My original container entrypoint.sh had logic that looked roughly like this:

if command -v iptables >/dev/null 2>&1; then
    apply_firewall_rules
else
    echo "Warning: iptables not found, continuing anyway..."
fi

exec run_proxy "$@"

If the container was accidentally started without the NET_ADMIN Linux capability, or if a base image update broke iptables, the script simply printed a warning to stdout and happily booted the proxy without a firewall.

This is classic "Fail-Open" design, and in security software, it is fatal. A security boundary that silently degrades to unprotected execution is worse than no boundary at all, because it gives you false confidence.

We rewrote the entire initialization script to be strictly Fail-Closed:

set -euo pipefail

if ! iptables -L -n >/dev/null 2>&1; then
    echo "[FATAL] iptables unavailable or insufficient permissions (missing NET_ADMIN?). Aborting." >&2
    exit 1
fi

apply_firewall_rules || {
    echo "[FATAL] Failed to enforce firewall policy. Aborting startup." >&2
    exit 1
}

Now, if a single firewall rule fails to apply, the container terminates immediately with exit code 1. The sandbox refuses to run without its armor.

During a full Docker rebuild, our proxy container suddenly crashed on boot:

mitmdump: error: unrecognized arguments: --termlog-verbosity

We had updated to mitmproxy 10.4.2, which completely deprecated the --termlog-verbosity CLI flag in favor of options syntax: --set termlog_verbosity=info.

Because our test suite was testing startup shell logic using mocked binaries. Our test mock for mitmdump was a dummy script that recorded incoming flags and exited 0 without validating whether those flags were actually accepted by the real mitmproxy CLI!

Our tests were falsely green.

We updated the configuration to the modern --set syntax, added negative assertions to the mocks to disallow deprecated arguments, and verified startup against the real binary.

To adhere to the principle of least privilege, our proxy container must drop root privileges after configuring iptables and run the worker process as an unprivileged user named proxy with explicit UID 1002.

In our Dockerfile, we had:

RUN groupadd -g 1002 proxy || true && \
    useradd -u 1002 -g proxy proxy || true

When checking running processes inside the container with docker top, something looked wrong:

$ docker top scamshield-proxy
UID     PID     PPID    CMD
13      14820   14798   mitmdump -s /app/addon.py ...

The process wasn't running as UID 1002. It was running as UID 13!

The official python:3.11-slim image is built on Debian. Debian has shipped with a built-in system user named proxy (UID 13) since the 1990s (originally for Squid/inn).

When our Dockerfile executed groupadd ... || true, it failed silently because group proxy already existed, and useradd skipped creation. The container fell back to the system user UID 13.

Never rely on implicit user creation in base images. We explicitly modify existing system users or pick a collision-free name:

RUN if id "proxy" >/dev/null 2>&1; then \
        userdel -f proxy && groupdel proxy || true; \
    fi && \
    groupadd -g 1002 proxy && \
    useradd -u 1002 -g proxy -s /usr/sbin/nologin -d /nonexistent proxy

Always inspect docker top and /proc/<pid>/status in the running container to verify that your process is actually running with the exact UID/GID you intended.

Component Status Details
Regression Test Suite 🟢 Passing 57/57 tests green across entrypoint logic, proxy addons, and preflight gates.
Firewall Startup 🟢 Verified Container boots as root, sets 19 iptables rules, verifies withiptables -C , drops to UID1002 , and startsmitmdump .
CLI Arguments 🟢 Verified Migrated to mitmproxy 10.x options syntax (--set ).
Live Network Egress Tests 🟡 In Progress Writing integration tests to confirm traffic to forbidden IPs actually drops at the packet level.

ip6tables policies to prevent IPv6 leak bypasses. This week, I am executing our Network Gate Verification Suite. Starting a container cleanly is not proof that packets are blocked; proof requires testing real network requests from inside the sandbox:

curl -m 2 http://169.254.169.254 curl -m 2 http://10.0.0.1 curl https://example.com Once the network gate passes, we begin Stage 2: The Browser Container (locking down Headless Chromium with Playwright).

Have you ever had a unit test suite stay completely green while your production container was fundamentally broken due to mocked tools? How do you structure your egress firewall rules for scraping or threat intelligence workloads?

Drop your thoughts in the comments below! 👇

Follow the project on GitHub: github.com/Siddharthrane07/scamshield

── more in #ai-safety 4 stories · sorted by recency
── more on @scamshield ai 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/building-scamshield-…] indexed:0 read:5min 2026-11-03 · —