From 307f6e3412e42951c6094039a441aa4158777f8f Mon Sep 17 00:00:00 2001 From: Buba Date: Mon, 8 Sep 2025 18:49:53 +0000 Subject: [PATCH] Isolate networks of sensitive services --- docker/backup/kopia-b2.yaml | 5 ++- docker/backup/kopia-nas.yaml | 5 ++- docker/dev/code-server.yaml | 5 ++- docker/guidelines.md | 67 +++++++++++++++++++++++++++-- docker/security/authelia.yaml | 5 ++- docker/security/endlessh.yaml | 6 --- docker/security/traefik.yaml | 15 +++++++ docker/security/traefik/traefik.yml | 2 +- docker/security/wg-easy.yaml | 5 ++- docker/tools/vaultwarden.yaml | 5 ++- scripts/labctl.py | 38 +++++++++++++++- 11 files changed, 135 insertions(+), 23 deletions(-) diff --git a/docker/backup/kopia-b2.yaml b/docker/backup/kopia-b2.yaml index ebee2109..d546c96d 100644 --- a/docker/backup/kopia-b2.yaml +++ b/docker/backup/kopia-b2.yaml @@ -52,10 +52,11 @@ services: - ${STORAGE_MEDIA}:/sources/nas/media:ro networks: - - proxy + - backup-kopia labels: traefik.enable: true + traefik.docker.network: backup-kopia traefik.http.routers.kopia-b2.middlewares: localaccess-sso@file traefik.http.services.kopia-b2.loadbalancer.server.port: 80 homepage.group: Storage @@ -65,5 +66,5 @@ services: homepage.description: "[SSO] Backups Using the Cloud Storage You Pick" networks: - proxy: + backup-kopia: external: true diff --git a/docker/backup/kopia-nas.yaml b/docker/backup/kopia-nas.yaml index bdb3d71e..c9407733 100644 --- a/docker/backup/kopia-nas.yaml +++ b/docker/backup/kopia-nas.yaml @@ -50,10 +50,11 @@ services: - ${STORAGE_BACKUP_ON_NAS}/kopia:/backup-on-nas networks: - - proxy + - backup-kopia labels: traefik.enable: true + traefik.docker.network: backup-kopia traefik.http.routers.kopia-nas.middlewares: localaccess-sso@file traefik.http.services.kopia-nas.loadbalancer.server.port: 80 homepage.group: Storage @@ -63,5 +64,5 @@ services: homepage.description: "[SSO] Backups Using the Cloud Storage You Pick" networks: - proxy: + backup-kopia: external: true diff --git a/docker/dev/code-server.yaml b/docker/dev/code-server.yaml index c655b5b5..6e4b7b94 100644 --- a/docker/dev/code-server.yaml +++ b/docker/dev/code-server.yaml @@ -30,9 +30,10 @@ services: - ${ADMIN_HOME}/repos:/config/repos - ${ADMIN_HOME}/.ssh:/config/.ssh networks: - - proxy + - dev-code-server labels: traefik.enable: true + traefik.docker.network: dev-code-server traefik.http.routers.code.middlewares: localaccess-sso@file traefik.http.routers.code.rule: Host(`code.${MYDOMAIN}`) traefik.http.services.code.loadbalancer.server.port: 8443 @@ -43,5 +44,5 @@ services: homepage.description: "[SSO] Visual Studio Code" networks: - proxy: + dev-code-server: external: true diff --git a/docker/guidelines.md b/docker/guidelines.md index 7fc0f74d..4b6d2393 100644 --- a/docker/guidelines.md +++ b/docker/guidelines.md @@ -35,7 +35,9 @@ networks: ### Networking -All services use a shared external `proxy` network for communication: +#### Shared Proxy Network + +By default, most services connect to the shared external `proxy` network for communication: ```yaml networks: @@ -43,6 +45,63 @@ networks: external: true ``` +#### Isolated Networks for Sensitive Services + +Sensitive services (like backup systems, password managers, VPN services) should use isolated networks to prevent direct communication between services, with only Traefik able to connect to them: + +```yaml +# Service YAML file - isolated network definition +networks: + service-category-name: + external: true + +# Service configuration +services: + service: + # ... other configuration + networks: + - service-category-name # Instead of 'proxy' + labels: + traefik.enable: true + traefik.docker.network: service-category-name # Tell Traefik which network to use + # ... other labels +``` + +Traefik must be connected to all isolated networks to route traffic: + +```yaml +# traefik.yaml +services: + traefik: + # ... other configuration + networks: + - proxy # Main network + - security-authelia # Isolated service networks + - security-wg-easy + - tools-vaultwarden + # ... other isolated networks +``` + +And in the networks section of the same file, all these external networks must be defined: + +```yaml +networks: + proxy: # Main network + external: true + security-authelia: # Isolated service networks + external: true + security-wg-easy: + external: true + tools-vaultwarden: + external: true + # ... other isolated networks +``` + +This architecture ensures that: +1. Sensitive services can only communicate with Traefik, not with each other +2. Services in isolated networks are not reachable from the shared proxy network +3. All external access still flows through Traefik's security controls + ### Traefik Reverse Proxy Traefik serves as the central reverse proxy for all services with the following features: @@ -178,9 +237,11 @@ services: - ${DOCKER_VOLUMES}/service-name:/data - ./service-name/config:/config # If needed networks: - - proxy + - proxy # Or service-specific network for sensitive services labels: traefik.enable: true + # If using isolated network, specify which network Traefik should use + # traefik.docker.network: service-category-name traefik.http.routers.service-name.middlewares: middleware-name@file traefik.http.services.service-name.loadbalancer.server.port: PORT homepage.group: Category @@ -190,6 +251,6 @@ services: homepage.description: "Service description" networks: - proxy: + proxy: # Or service-specific network for sensitive services external: true ``` diff --git a/docker/security/authelia.yaml b/docker/security/authelia.yaml index 8c4beb75..5fe87e06 100644 --- a/docker/security/authelia.yaml +++ b/docker/security/authelia.yaml @@ -42,12 +42,13 @@ services: expose: - 9091 networks: - - proxy + - security-authelia depends_on: initContainer: condition: service_completed_successfully labels: traefik.enable: true + traefik.docker.network: security-authelia traefik.http.routers.authelia.rule: Host(`auth.${MYDOMAIN}`) traefik.http.routers.authelia.middlewares: localaccess@file traefik.http.services.authelia.loadbalancer.server.port: 9091 @@ -58,5 +59,5 @@ services: homepage.description: "[SSO] Open-source authentication and authorization" networks: - proxy: + security-authelia: external: true diff --git a/docker/security/endlessh.yaml b/docker/security/endlessh.yaml index 43efa5e1..9993f509 100644 --- a/docker/security/endlessh.yaml +++ b/docker/security/endlessh.yaml @@ -33,14 +33,8 @@ services: ports: - 2222:2222 # Prometheus metrics port: 2112 - networks: - - proxy labels: homepage.group: Security homepage.name: Endlessh homepage.icon: terminal.png homepage.description: "SSH tarpit (port 2222)" - -networks: - proxy: - external: true diff --git a/docker/security/traefik.yaml b/docker/security/traefik.yaml index e993cf30..85556ca2 100644 --- a/docker/security/traefik.yaml +++ b/docker/security/traefik.yaml @@ -34,6 +34,11 @@ services: - 443:443/udp # HTTP/3 - QUIC networks: - proxy + - security-authelia + - security-wg-easy + - dev-code-server + - tools-vaultwarden + - backup-kopia extra_hosts: - host.docker.internal:host-gateway labels: @@ -67,3 +72,13 @@ services: networks: proxy: external: true + security-authelia: + external: true + security-wg-easy: + external: true + dev-code-server: + external: true + tools-vaultwarden: + external: true + backup-kopia: + external: true diff --git a/docker/security/traefik/traefik.yml b/docker/security/traefik/traefik.yml index 47f38dee..f5181c3e 100644 --- a/docker/security/traefik/traefik.yml +++ b/docker/security/traefik/traefik.yml @@ -29,7 +29,7 @@ providers: # Docker provider for connecting all apps that are inside of the docker network docker: watch: true - network: proxy + network: proxy # Can be overridden on a per-container basis with the traefik.docker.network label defaultRule: 'Host(`{{ index .Labels "com.docker.compose.service"}}.{{env "MYDOMAIN"}}`)' # Default host rule: containername.domain.tld exposedByDefault: false # Do not expose containers to the outside world unless explicitly configured diff --git a/docker/security/wg-easy.yaml b/docker/security/wg-easy.yaml index 3f28d2fd..eddcaf84 100644 --- a/docker/security/wg-easy.yaml +++ b/docker/security/wg-easy.yaml @@ -32,9 +32,10 @@ services: volumes: - ${DOCKER_VOLUMES}/wg-easy:/etc/wireguard networks: - - proxy + - security-wg-easy labels: traefik.enable: true + traefik.docker.network: security-wg-easy traefik.http.routers.wireguard.rule: Host(`vpn.${MYDOMAIN}`) traefik.http.routers.wireguard.middlewares: localaccess-sso@file traefik.http.services.wireguard.loadbalancer.server.port: 51821 @@ -45,5 +46,5 @@ services: homepage.description: "[SSO] VPN Service" networks: - proxy: + security-wg-easy: external: true diff --git a/docker/tools/vaultwarden.yaml b/docker/tools/vaultwarden.yaml index 165d08d0..bbce73a7 100644 --- a/docker/tools/vaultwarden.yaml +++ b/docker/tools/vaultwarden.yaml @@ -19,9 +19,10 @@ services: volumes: - ${DOCKER_VOLUMES}/vaultwarden:/data networks: - - proxy + - tools-vaultwarden labels: traefik.enable: true + traefik.docker.network: tools-vaultwarden traefik.http.routers.vault.middlewares: localaccess@file traefik.http.routers.vault.rule: Host(`vault.${MYDOMAIN}`) traefik.http.services.vault.loadbalancer.server.port: 80 @@ -32,5 +33,5 @@ services: homepage.description: Password Manager networks: - proxy: + tools-vaultwarden: external: true diff --git a/scripts/labctl.py b/scripts/labctl.py index 5f8d1bb8..bd6b90e3 100755 --- a/scripts/labctl.py +++ b/scripts/labctl.py @@ -32,6 +32,40 @@ def create_network_if_missing(network_name: str) -> None: docker(["network", "create", "--driver", "bridge", network_name]) +def get_external_networks(compose_file: Path) -> list[str]: + """Extract external networks from a Docker Compose file.""" + networks: list[str] = [] + try: + with open(compose_file) as f: + yaml_content = yaml.safe_load(f) or {} + networks_def = yaml_content.get("networks") or {} + if isinstance(networks_def, dict): + for key, cfg in networks_def.items(): + if not isinstance(cfg, dict): + continue + ext = cfg.get("external", False) + # Support: external: true | external: {name: "..."} | name: "..." + name_override = cfg.get("name") + if ext is True: + networks.append(name_override or key) + elif isinstance(ext, dict): + networks.append(ext.get("name") or name_override or key) + except (FileNotFoundError, yaml.YAMLError, OSError) as e: + logger.warning(f"Error extracting networks from {compose_file}: {e}") + return networks + + +def create_service_networks(compose_file: Path) -> None: + """Create all external networks required by a service. + + Args: + compose_file: Path to the Docker Compose file + """ + networks = get_external_networks(compose_file) + for network_name in networks: + create_network_if_missing(network_name) + + def create_localhost_link(docker_config_dir: Path) -> None: """Create 'localhost' symlink in the parent directory.""" hostname = socket.gethostname() @@ -111,6 +145,9 @@ def docker_command(host_config_dir: Path, stack_dir: Path, service_name: str, ac logger.error(f"Compose file not found: {compose_file}") return + # Ensure all required networks exist before executing any Docker Compose command + create_service_networks(compose_file) + env_file_args = get_env_file_args(host_config_dir, service_name) # Handle other operations @@ -209,7 +246,6 @@ def cmd_config_apply(args) -> None: logger.info("Init...") config = load_services_config(config_file) create_localhost_link(host_config_dir.parent) - create_network_if_missing("proxy") # Process services with optional mode override process_services(host_config_dir, config, args.mode, args.pull_before_start)