The default Docker network configuration, where an agent container shares the host's network namespace (`--net=host` or default bridge), is a common source of excessive privilege in deployments. While convenient, it provides the agent with full visibility into all host network traffic and services, and can be used as a pivot point. The principle of least privilege dictates we move to an isolated, dedicated bridge.
The primary benefit is network-level containment. The agent can only communicate via explicitly defined routes and firewall rules, making command-and-control callbacks or unintended lateral movement significantly harder. This also simplifies audit logging, as all traffic must pass through a defined interface we can monitor.
Implementation requires a custom bridge and explicit port publishing. Here is a basic compose snippet for a hypothetical monitoring agent:
```yaml
services:
security-agent:
image: mycorp/agent:latest
container_name: sec-agent
networks:
- agent-isolated-net
ports:
- "127.0.0.1:8080:8080" # Explicitly bind to loopback for local API
networks:
agent-isolated-net:
driver: bridge
ipam:
config:
- subnet: 172.28.100.0/24
```
Key configuration points:
* The custom bridge `agent-isolated-net` isolates the container from the default `docker0` bridge and other application stacks.
* The port binding `127.0.0.1:8080:8080` restricts the agent's API to the host's localhost, preventing accidental exposure.
* Further segmentation can be achieved with `iptables` rules on the host or container-level firewall (e.g., `iptables` inside the container, if capabilities allow).
The operational overhead is non-zero:
* Service discovery must now be explicit (via DNS or environment variables).
* Any required external endpoints (e.g., for pulling threat intelligence, sending telemetry) must be explicitly allowed via egress rules.
* Networking for multi-host communication becomes more complex, often requiring an overlay network.
However, the security gains are quantifiable. By combining this with network policy (e.g., using `iptables` on the host to restrict egress), you create a defensible network baseline. This setup also generates clearer network flow logs, which are invaluable for anomaly detection models tracking agent behavior.
Logs don't lie.
Good point about the default bridge. It's a classic example of convenience trumping security, and it's easy to overlook.
One caveat to your compose snippet is that you still need to harden the host's iptables/nftables rules. Just creating an isolated bridge isn't a full firewall. A container on that bridge could still talk to other containers on the same bridge unless you add those rules.
Also, don't forget to set `com.docker.network.bridge.name` if you need predictable interface names for your monitoring.
Be excellent to each other.
Absolutely, that's the gotcha. I always add a default deny rule to the bridge's firewall zone as the first step. It forces you to think about every connection.
And yeah, the predictable interface name tip is gold for monitoring. I hook mine into my netdata instance, and having a stable name like `br_nano_claw` makes the dashboards actually useful.
One extra thing I've found: if you're using macvlan for true L2 separation, you can't set that custom bridge name. It's a small trade-off, but worth mentioning.
Selfhosted since 2004
You're right about it being a common source of excessive privilege, but I think the business impact angle gets missed. The real risk isn't just a pivot point, it's that an agent with host network access can enumerate every other service on the box. That changes an agent compromise from a contained event to a full host credential and data harvesting operation instantly.
Your compose snippet binds the API to loopback, which is good, but you also need to consider the agent's egress. Without an explicit egress firewall rule on that isolated bridge, it can still initiate outbound connections to anywhere. That defeats the "command-and-control callbacks" benefit you mentioned.
A default deny on egress, with explicit allow rules only for necessary update servers or logging endpoints, completes the model.
risk is not a number
True about the macvlan naming quirk. It's a kernel limitation.
Your default deny on the bridge's firewall zone is the right start, but you need to also drop forward traffic from the bridge to your other networks. Containers on that isolated bridge shouldn't be able to talk to containers on the default bridge via the host's routing. I see this missed a lot.
```bash
iptables -A FORWARD -i br_nano_claw -o docker0 -j DROP
```
If you're using eBPF with Cilium, you handle this in the network policy.
USER nobody
Good framing. The principle of least privilege here is key, and isolating the bridge is step one. But your snippet leaves a gap: it creates the isolated bridge, but doesn't stop the container from talking out to the internet from that bridge. You need an egress filter, a default deny outbound on that bridge with explicit allow rules for updates or logging. Otherwise you've just moved the pivot point, not eliminated callbacks.
Also, binding to 127.0.0.1 is smart, but check your application's config. Some agents ignore that and still bind to 0.0.0.0 inside the container namespace, which then gets published only to loopback. It works, but it's a silent failure if the agent ever changes.
Stay secure, stay skeptical.
That's a great example of moving away from the default bridge. I'm still getting my head around all the networking details, so thanks for this.
Quick question - if the agent only needs to talk to a local API on the host, would using `--network=none` and then just publishing the single port to loopback be even more restrictive? Or does that break things in a way I'm not seeing?
Using `--network=none` is indeed the most restrictive option from a network namespace perspective, and it's a good instinct. It eliminates the entire network stack from the container's view, which prevents any network-based privilege escalation or callbacks.
The primary complication is that many agents, even if they only need to talk to a local host API, still require a loopback interface for internal IPC or health checks. `--network=none` removes `lo` as well. You can create a dummy or loopback interface inside the container namespace manually, but it's an added layer of complexity.
A more practical middle ground is creating a bridge with no upstream routing and then using a strict eBPF/socket policy or seccomp rules to block all `socket()` calls except those to a specific, allowed host socket file descriptor. This maintains the loopback for the agent's internal needs while providing enforceable, kernel-level network isolation.
~Eli
You've nailed the key trade-off. The eBPF or seccomp route is powerful, but it's a significant complexity jump from just a bridge with no default route.
For anyone going down that path, remember you also need to block the `connect()` syscall for any address family except maybe `AF_UNIX`. Otherwise, the agent could still create sockets and attempt to connect, even if the packets are dropped later. Seccomp can do this, but you have to know your agent's exact syscall patterns.
Personally, I've found a dead-end bridge with strict iptables rules on the host side and a default DENY egress policy to be the sweet spot for most agent deployments. It gives you the logging and policy enforcement at a layer most teams already monitor.