Skip to content

Plan: Networking Hardening

Status

State: Active Started: 2026-05-14

Context

All 14 containers on sepia communicate over the Docker default bridge network. This provides no network isolation between services. A container compromise in any service (e.g. a web app RCE) can reach every other container.

Additionally, two services use network_mode: host: - homeassistant — needs host network for mDNS, UPnP, and hardware access (Zigbee sticks) - esphome — needs host network for mDNS discovery of ESP devices

No services use custom networks. All inter-service communication (e.g., dsmr → dsmrdb, grafana → influxdb) happens over the default bridge using container names.

Shuttle has the same plan as active/networking-hardening.md and has a full implementation.

Goals

  • [ ] Design network topology with functional isolation
  • [ ] Implement custom networks in compose files
  • [ ] Assign services to appropriate networks
  • [ ] Caddy gets access to all networks it needs to proxy
  • [ ] Document network_mode: host exceptions and risks

Steps

Step 1: Design Network Topology

Define networks by function — services only connect to networks they need:

networks:
  frontend:      # Reverse proxy accessible (Caddy + proxied services)
  backend:       # Internal APIs, no direct external access
  storage:       # Databases
  monitoring:    # Metrics collection
  sensors:       # ESP devices

Proposed mapping:

Network Services Purpose
frontend caddy, grafana, homeassistant, dsmr, seafile-server, docs Caddy needs to proxy to these
backend dsmr, dsmrdb, influxdb, timescaledb, seafile-server, seafile-mysql, seafile-redis, grafana Inter-service API calls
storage dsmrdb, influxdb, timescaledb, seafile-mysql, seafile-redis Database-only network
monitoring collectd, influxdb, grafana Metrics pipeline
sensors esphome ESP device communication

Note: Services can belong to multiple networks (e.g., grafana is on frontend, backend, storage, and monitoring).

Step 2: Define Networks in compose.yaml

Add to the main /opt/compose.yaml:

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
  storage:
    driver: bridge
  monitoring:
    driver: bridge
  sensors:
    driver: bridge

Step 3: Assign Services to Networks

Update each compose.<service>.yaml:

Example — grafana:

services:
  grafana:
    networks:
      - frontend      # Caddy proxies to it
      - backend       # DSMR data source queries
      - storage       # InfluxDB/TimescaleDB queries
      - monitoring    # Collectd metrics

Example — dsmr:

services:
  dsmr:
    networks:
      - frontend      # Caddy proxies to it
      - backend       # API access
      - storage       # Database access

  dsmrdb:
    networks:
      - storage       # Only accessible from backend services

Step 4: Handle network_mode: host Services

homeassistant and esphome use network_mode: host. This can't easily be replaced because: - Home Assistant needs host networking for mDNS, UPnP discovery, and potential USB Zigbee/Bluetooth dongles - ESPHome needs host networking for mDNS ESP device discovery

Document the risk: - These services have full host network access (no isolation) - If compromised, attacker has full LAN access from the host - Mitigation: keep them updated, minimize exposed ports, use Caddy auth

Consider macvlan/ipvlan as future improvement:

services:
  homeassistant:
    networks:
      ha_net:
        ipv4_address: 192.168.2.50  # Dedicated IP on LAN

networks:
  ha_net:
    driver: macvlan
    driver_opts:
      parent: enp2s0
    ipam:
      config:
        - subnet: "192.168.2.0/24"
          gateway: "192.168.2.1"

This would give homeassistant its own LAN IP without host networking. However, macvlan has limitations (no host-to-container communication without a second interface). Defer to a future plan.

Step 5: Update DNS Resolution

With custom networks, Docker's embedded DNS resolves container names automatically within each network. Services on different networks can't reach each other by name — this is the intended isolation.

Cross-network communication goes through Caddy (the reverse proxy), which is attached to all networks it needs to proxy to.

  • Verification: docker compose config validates network assignments
  • Verification: Grafana can still query InfluxDB and TimescaleDB
  • Verification: Caddy can proxy to all frontend services
  • Verification: Containers on different networks cannot ping each other

Rollback

Remove network assignments and the networks: top-level block from compose files. git checkout -- compose.*.yaml reverts.

  • REFERENCE/network.md (update with network topology)
  • REFERENCE/services.md (update with network assignments)
  • PLANS/active/compose-best-practices.md
  • PLANS/active/dns-ad-blocker-migration.md (new DNS resolver may affect networking)

Created: 2026-05-14