diff --git a/AGENTS.md b/AGENTS.md index c5e3955..6f24ff0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,18 +51,34 @@ All services live under `zakobar.com`. | Jellyfin | `jellyfin.zakobar.com` | Jellyfin-native | | Transmission | `torrent.zakobar.com` | Authelia two_factor, admins only | -Internal ports (all bound to `127.0.0.1`): +## Networking -| Container | Port | -|-----------|------| -| openldap | 389 | -| authelia | 9091 | -| gitea | 3000 | -| nextcloud | 8080 | -| nextcloud-postgres | 5432 | -| phpldapadmin | 8081 | -| jellyfin | 8096 | -| transmission | 9092 (not 9091 — avoids clash with authelia) | +All containers join a private podman network named **`homey`**, created by the +`podman-homey-network` systemd service in `common.nix`. This provides: + +- **DNS isolation** — containers reach each other by name (e.g. `openldap`, + `nextcloud-postgres`) without being exposed on the host network. +- **No port conflicts** — Caddy owns host ports 80/443; service containers map + only to `127.0.0.1:`. +- **Defence in depth** — even if the firewall were misconfigured, services are + not bound to `0.0.0.0`. + +Internal ports (all mapped to `127.0.0.1` on the host): + +| Container | Host port | Container port | +|-----------|-----------|----------------| +| openldap | 389 | 389 | +| authelia | 9091 | 9091 | +| gitea | 3000 | 3000 | +| nextcloud | 8080 | 80 | +| nextcloud-postgres | 5432 | 5432 | +| phpldapadmin | 8081 | 80 | +| jellyfin | 8096 | 8096 | +| transmission | 9092 | 9091 | + +Inter-container communication uses container names on the `homey` network +(e.g. authelia → `ldap://openldap:389`, nextcloud → `nextcloud-postgres:5432`). +Caddy (running on the host) proxies via `127.0.0.1:`. ## Storage Layout @@ -163,8 +179,9 @@ restic password, Cloudflare tokens) can be generated fresh. file at `/run/-secrets.env` and reference it via `EnvironmentFile`. Clean it up in `postStop`. -5. **`--network=host`** — all containers use host networking for simplicity on - a single-node setup. Services communicate via `127.0.0.1:`. +5. **`--network=homey`** — all containers join the private `homey` podman + network. Inter-container traffic uses container names as hostnames; host + access is via explicit `ports` mappings to `127.0.0.1:`. 6. **Systemd ordering** — always express `after`/`requires` dependencies explicitly. The external HD mount unit is `mnt-data.mount`; containers that diff --git a/TODO.org b/TODO.org index c9fef3f..1105ce2 100644 --- a/TODO.org +++ b/TODO.org @@ -198,7 +198,8 @@ ** DONE Configure Gitea LDAP authentication Admin → Site Administration → Authentication Sources → Add LDAP (via BindDN): - - Host: =127.0.0.1=, Port: =389=, Security: Unencrypted + - Host: =openldap=, Port: =389=, Security: Unencrypted + (containers talk via the =homey= podman network — use container name, not =127.0.0.1=) - Bind DN: =cn=readonly,dc=zakobar,dc=com= - Bind Password: see =openldap/ro_password= in sops - User Search Base: =ou=users,dc=zakobar,dc=com= @@ -213,7 +214,8 @@ Admin → LDAP/AD Integration — confirm the LDAP Users and Contacts app is configured. If reconfiguring from scratch, use the same settings as Gitea above but with Nextcloud's LDAP wizard: - - Server: =127.0.0.1=, Port: =389= + - Server: =openldap=, Port: =389= + (container name on the =homey= network — not =127.0.0.1=) - Bind DN: =cn=readonly,dc=zakobar,dc=com= - Bind Password: see =openldap/ro_password= in sops - Base DN: =dc=zakobar,dc=com= diff --git a/modules/backup.nix b/modules/backup.nix index 8e3f248..8d882f1 100644 --- a/modules/backup.nix +++ b/modules/backup.nix @@ -87,16 +87,17 @@ in Type = "oneshot"; ExecStart = pkgs.writeShellScript "backup-pre" '' set -euo pipefail + podman="${pkgs.podman}/bin/podman" # Put Nextcloud into maintenance mode (if running) if systemctl is-active --quiet podman-nextcloud.service; then - podman exec nextcloud php occ maintenance:mode --on || true + $podman exec nextcloud php occ maintenance:mode --on || true fi # Dump postgres (if running) if systemctl is-active --quiet podman-nextcloud-postgres.service; then install -d -m 700 ${dataDir}/nextcloud/db-dump - podman exec nextcloud-postgres \ + $podman exec nextcloud-postgres \ pg_dump -U postgres nextcloud_db \ > ${dataDir}/nextcloud/db-dump/nextcloud.sql fi @@ -111,7 +112,7 @@ in ExecStart = pkgs.writeShellScript "backup-post" '' set -euo pipefail if systemctl is-active --quiet podman-nextcloud.service; then - podman exec nextcloud php occ maintenance:mode --off || true + ${pkgs.podman}/bin/podman exec nextcloud php occ maintenance:mode --off || true fi ''; }; diff --git a/modules/caddy.nix b/modules/caddy.nix index bcdbb47..e2ab77d 100644 --- a/modules/caddy.nix +++ b/modules/caddy.nix @@ -142,7 +142,9 @@ in max_size 5GB } - reverse_proxy localhost:8080 + reverse_proxy localhost:8080 { + header_up X-Forwarded-For {remote_host} + } ''; }; "http://nextcloud.${domain}" = { @@ -154,6 +156,7 @@ in } reverse_proxy localhost:8080 { header_up X-Forwarded-Proto https + header_up X-Forwarded-For {remote_host} } ''; }; diff --git a/modules/common.nix b/modules/common.nix index 3f7129b..8307d51 100644 --- a/modules/common.nix +++ b/modules/common.nix @@ -80,6 +80,26 @@ defaultNetwork.settings.dns_enabled = true; }; + # Create the shared "homey" podman network that all service containers join. + # DNS is enabled by default on netavark-backed networks, so containers can + # reach each other by container name (e.g. "openldap", "nextcloud-postgres"). + systemd.services.podman-homey-network = { + description = "Create homey podman network"; + wantedBy = [ "multi-user.target" ]; + before = [ "podman-openldap.service" "podman-authelia.service" + "podman-gitea.service" "podman-nextcloud-postgres.service" + "podman-nextcloud.service" "podman-phpldapadmin.service" + "podman-jellyfin.service" "podman-transmission.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = pkgs.writeShellScript "create-homey-network" '' + ${pkgs.podman}/bin/podman network exists homey \ + || ${pkgs.podman}/bin/podman network create homey + ''; + }; + }; + # ------------------------------------------------------------------------- # Core packages available on every host # ------------------------------------------------------------------------- diff --git a/modules/services/authelia.nix b/modules/services/authelia.nix index 5ed42a2..412468c 100644 --- a/modules/services/authelia.nix +++ b/modules/services/authelia.nix @@ -43,7 +43,7 @@ let authentication_backend: ldap: implementation: "custom" - url: "ldap://127.0.0.1:389" + url: "ldap://openldap:389" timeout: "5s" start_tls: false base_dn: "${ldapBaseDN}" @@ -162,7 +162,7 @@ in virtualisation.oci-containers.containers.authelia = { image = cfg.image; - # No ports mapping — --network=host shares the host network stack directly. + ports = [ "127.0.0.1:${toString cfg.port}:9091" ]; environment = { TZ = homeyConfig.timezone; @@ -184,7 +184,7 @@ in ]; extraOptions = [ - "--network=host" + "--network=homey" "--hostname=authelia" ]; }; @@ -193,8 +193,8 @@ in # Systemd — wait for openldap and external HD # ----------------------------------------------------------------------- systemd.services."podman-authelia" = { - after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ]; - requires = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ]; + after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" "podman-homey-network.service" ]; + requires = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" "podman-homey-network.service" ]; }; }; } diff --git a/modules/services/gitea.nix b/modules/services/gitea.nix index 059cc0b..efa818c 100644 --- a/modules/services/gitea.nix +++ b/modules/services/gitea.nix @@ -60,8 +60,7 @@ in # ----------------------------------------------------------------------- virtualisation.oci-containers.containers.gitea = { image = cfg.image; - # No ports mapping — --network=host means the container shares the host - # network stack directly. Gitea binds to 0.0.0.0:3000 on the host. + ports = [ "127.0.0.1:${toString cfg.port}:3000" ]; # All non-secret settings via GITEA__
__ env vars. # These are safe to store in the Nix store. @@ -153,7 +152,7 @@ in "${dataDir}/gitea/data:/data" ]; - extraOptions = [ "--network=host" ]; + extraOptions = [ "--network=homey" ]; }; # ----------------------------------------------------------------------- @@ -182,8 +181,8 @@ in '') ]; }; - after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ]; - requires = lib.mkAfter [ "mnt-data.mount" ]; + after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" "podman-homey-network.service" ]; + requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; }; # ----------------------------------------------------------------------- diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix index ff2a006..aea74c1 100644 --- a/modules/services/jellyfin.nix +++ b/modules/services/jellyfin.nix @@ -30,7 +30,7 @@ in config = lib.mkIf cfg.enable { virtualisation.oci-containers.containers.jellyfin = { image = cfg.image; - # No ports mapping — --network=host shares the host network stack directly. + ports = [ "127.0.0.1:${toString cfg.port}:8096" ]; environment = { JELLYFIN_PublishedServerUrl = "https://jellyfin.${domain}"; @@ -44,12 +44,12 @@ in "${dataDir}/media/tvshows:/data/tvshows:ro" ]; - extraOptions = [ "--network=host" ]; + extraOptions = [ "--network=homey" ]; }; systemd.services."podman-jellyfin" = { - after = lib.mkAfter [ "mnt-data.mount" ]; - requires = lib.mkAfter [ "mnt-data.mount" ]; + after = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; + requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; }; }; } diff --git a/modules/services/nextcloud.nix b/modules/services/nextcloud.nix index 6b69a16..a21b9f1 100644 --- a/modules/services/nextcloud.nix +++ b/modules/services/nextcloud.nix @@ -58,7 +58,9 @@ in # ----------------------------------------------------------------------- virtualisation.oci-containers.containers.nextcloud-postgres = { image = cfg.postgresImage; - # No ports mapping — --network=host shares the host network stack directly. + # Exposed on localhost for debugging; nextcloud reaches it via the + # container name "nextcloud-postgres" on the homey network. + ports = [ "127.0.0.1:${toString cfg.postgresPort}:5432" ]; environment = { POSTGRES_DB = "nextcloud_db"; @@ -71,7 +73,7 @@ in ]; extraOptions = [ - "--network=host" + "--network=homey" "--env-file=/run/nc-postgres-secrets.env" ]; }; @@ -91,8 +93,8 @@ in ]; }; postStop = "rm -f /run/nc-postgres-secrets.env"; - after = lib.mkAfter [ "mnt-data.mount" ]; - requires = lib.mkAfter [ "mnt-data.mount" ]; + after = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; + requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; }; # ----------------------------------------------------------------------- @@ -100,20 +102,22 @@ in # ----------------------------------------------------------------------- virtualisation.oci-containers.containers.nextcloud = { image = cfg.image; - # No ports mapping — --network=host shares the host network stack directly. + # Apache inside the container listens on port 80; map it to cfg.port on + # the host so Caddy can reach it. Postgres is reachable by container name. + ports = [ "127.0.0.1:${toString cfg.port}:80" ]; environment = { - POSTGRES_HOST = "127.0.0.1"; + POSTGRES_HOST = "nextcloud-postgres"; POSTGRES_DB = "nextcloud_db"; POSTGRES_USER = "postgres"; NEXTCLOUD_ADMIN_USER = "admin"; NEXTCLOUD_TRUSTED_DOMAINS = "nextcloud.${domain}"; OVERWRITEPROTOCOL = "https"; OVERWRITECLIURL = "https://nextcloud.${domain}"; - # With --network=host, port mappings are ignored and the container's - # Apache binds directly on the host. Force it onto port 8080 so Caddy - # can own 80/443. - APACHE_HTTP_PORT_NUMBER = toString cfg.port; + OVERWRITEHOST = "nextcloud.${domain}"; + # Trust the reverse proxy (Caddy on the host reaches the container + # via the podman bridge; cover all RFC-1918 ranges to be robust). + TRUSTED_PROXIES = "10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 127.0.0.1 ::1"; # Passwords injected via env file }; @@ -122,7 +126,7 @@ in ]; extraOptions = [ - "--network=host" + "--network=homey" "--env-file=/run/nc-secrets.env" ]; }; @@ -143,8 +147,8 @@ in ]; }; postStop = "rm -f /run/nc-secrets.env"; - after = lib.mkAfter [ "mnt-data.mount" "podman-nextcloud-postgres.service" ]; - requires = lib.mkAfter [ "mnt-data.mount" "podman-nextcloud-postgres.service" ]; + after = lib.mkAfter [ "mnt-data.mount" "podman-nextcloud-postgres.service" "podman-homey-network.service" ]; + requires = lib.mkAfter [ "mnt-data.mount" "podman-nextcloud-postgres.service" "podman-homey-network.service" ]; }; }; } diff --git a/modules/services/openldap.nix b/modules/services/openldap.nix index e1543e5..1d13dfb 100644 --- a/modules/services/openldap.nix +++ b/modules/services/openldap.nix @@ -50,10 +50,7 @@ in virtualisation.oci-containers.containers.openldap = { image = cfg.image; - # No ports mapping — --network=host means the container shares the host - # network stack. OpenLDAP binds to 0.0.0.0:389, but the firewall - # (common.nix) only opens 22/80/443, so port 389 is unreachable from - # the LAN or internet. + ports = [ "127.0.0.1:${toString cfg.port}:389" ]; environment = { LDAP_ORGANISATION = homeyConfig.organization; @@ -78,7 +75,7 @@ in ]; extraOptions = [ - "--network=host" + "--network=homey" "--env-file=/run/openldap-secrets.env" ]; }; @@ -113,8 +110,8 @@ in # Clean up the env file on stop postStop = "rm -f /run/openldap-secrets.env"; # Wait for the external HD to be mounted before starting - after = lib.mkAfter [ "mnt-data.mount" ]; - requires = lib.mkAfter [ "mnt-data.mount" ]; + after = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; + requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; }; # ----------------------------------------------------------------------- diff --git a/modules/services/phpldapadmin.nix b/modules/services/phpldapadmin.nix index 296c317..2fe0fc1 100644 --- a/modules/services/phpldapadmin.nix +++ b/modules/services/phpldapadmin.nix @@ -36,19 +36,18 @@ in environment = { PHPLDAPADMIN_HTTPS = "false"; - # host.containers.internal resolves to the host from inside a podman - # bridge container — reaches openldap which is on --network=host at :389 - PHPLDAPADMIN_LDAP_HOSTS = "host.containers.internal"; + # "openldap" resolves to the OpenLDAP container via homey network DNS. + PHPLDAPADMIN_LDAP_HOSTS = "openldap"; }; - # Bridge network (default) + port mapping: Apache binds inside the - # container on :80, podman maps it to 127.0.0.1:8081 on the host. ports = [ "127.0.0.1:${toString cfg.port}:80" ]; + + extraOptions = [ "--network=homey" ]; }; systemd.services."podman-phpldapadmin" = { - after = lib.mkAfter [ "podman-openldap.service" ]; - wants = lib.mkAfter [ "podman-openldap.service" ]; + after = lib.mkAfter [ "podman-openldap.service" "podman-homey-network.service" ]; + wants = lib.mkAfter [ "podman-openldap.service" "podman-homey-network.service" ]; }; }; } diff --git a/modules/services/transmission.nix b/modules/services/transmission.nix index c984a5f..218acb7 100644 --- a/modules/services/transmission.nix +++ b/modules/services/transmission.nix @@ -35,16 +35,14 @@ in config = lib.mkIf cfg.enable { virtualisation.oci-containers.containers.transmission = { image = cfg.image; - # No ports mapping — --network=host shares the host network stack directly. + # Map host cfg.port (9092) → container 9091 so Caddy can reach it + # without conflicting with Authelia's host port (also 9091). + ports = [ "127.0.0.1:${toString cfg.port}:9091" ]; environment = { PUID = "1000"; PGID = "1000"; - # With --network=host, port mappings are ignored; transmission binds - # directly on the host. Force it to cfg.port (9092) to avoid - # conflicting with Authelia on 9091. TRANSMISSION_WEB_HOME = "/usr/share/transmission/web"; - WEBUI_PORT = toString cfg.port; }; volumes = [ @@ -55,12 +53,12 @@ in "${dataDir}/media/complete:/downloads/complete" ]; - extraOptions = [ "--network=host" ]; + extraOptions = [ "--network=homey" ]; }; systemd.services."podman-transmission" = { - after = lib.mkAfter [ "mnt-data.mount" ]; - requires = lib.mkAfter [ "mnt-data.mount" ]; + after = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; + requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; }; }; } diff --git a/modules/storage.nix b/modules/storage.nix index 3b25644..e939b3c 100644 --- a/modules/storage.nix +++ b/modules/storage.nix @@ -88,8 +88,11 @@ in "d ${cfg.mountPoint}/gitea 0750 1000 1000 -" "d ${cfg.mountPoint}/gitea/data 0750 1000 1000 -" "d ${cfg.mountPoint}/nextcloud 0750 root root -" - "d ${cfg.mountPoint}/nextcloud/html 0750 root root -" - "d ${cfg.mountPoint}/nextcloud/db 0750 root root -" + # www-data in the Nextcloud container is UID 33; it needs rx on the + # directory and rw on all files it creates inside. + "d ${cfg.mountPoint}/nextcloud/html 0750 33 33 -" + # Postgres (uid 999) must own this directory — it creates files directly in it + "d ${cfg.mountPoint}/nextcloud/db 0700 999 999 -" "d ${cfg.mountPoint}/jellyfin 0750 root root -" "d ${cfg.mountPoint}/jellyfin/config 0750 root root -" "d ${cfg.mountPoint}/media 0755 root root -" diff --git a/shells/defaultShell.nix b/shells/defaultShell.nix index 6977c5a..c662dfe 100644 --- a/shells/defaultShell.nix +++ b/shells/defaultShell.nix @@ -7,7 +7,7 @@ pkgs.mkShell { --flake .#pi-main \ --target-host admin@192.168.1.100 \ --build-host admin@192.168.1.100 \ - --sudo + --use-remote-sudo '') (pkgs.writeShellScriptBin "homey-build-rpi-main" '' sudo nixos-rebuild switch \