diff options
Diffstat (limited to 'sys/nspawn')
| -rw-r--r-- | sys/nspawn/default.nix | 5 | ||||
| -rw-r--r-- | sys/nspawn/dmz.nix | 236 |
2 files changed, 241 insertions, 0 deletions
diff --git a/sys/nspawn/default.nix b/sys/nspawn/default.nix new file mode 100644 index 0000000..15e60de --- /dev/null +++ b/sys/nspawn/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./dmz.nix + ]; +} diff --git a/sys/nspawn/dmz.nix b/sys/nspawn/dmz.nix new file mode 100644 index 0000000..626993d --- /dev/null +++ b/sys/nspawn/dmz.nix @@ -0,0 +1,236 @@ +{ + config, + lib, + pkgs, + flakes, + doctrine, + ... +}: +with lib; let + cfg = config.local.nspawn.dmz; + inherit (config.local) mailHost; + + dmzNet = config.local.nets.${cfg.netName}; + + hassPort = config.services.home-assistant.config.http.server_port; + hassEnable = config.local.home-assistant.enable; +in { + options.local.nspawn.dmz = { + enable = mkEnableOption "DMZ services in a container"; + + dns64 = mkOption { + type = types.str; + }; + + netName = mkOption { + type = types.str; + }; + + net6 = mkOption { + type = types.str; + readOnly = true; + }; + + hostAddr6 = mkOption { + type = types.str; + readOnly = true; + }; + + mtaAddr6 = mkOption { + type = types.str; + readOnly = true; + }; + + system = mkOption { + type = types.raw; + }; + }; + + # Situación con os-release + # + # La idea aquí es poder hacer 'btrfs subvol create /var/lib/machines/foo' y + # dejar que systemd-nspawn y el activation script creen todo lo demás. Esto + # no sirve bien debido a la prueba barata que hace systemd para revisar si el + # árbol parece contener una imagen de sistema operativo. Esta prueba falla en + # dos momentos distintos: + # + # 1. Inmediatamente tras crear un árbol vacío, puesto que os-release no existe. + # La solución naive es 'mkdir rootfs/etc && touch rootfs/etc/os-release'. + # + # 2. Luego de reiniciar el contenedor una vez que NixOS ha preparado /etc, ya que + # systemd espera un archivo regular y no telera el symlink a la store. + # + # Resulta ser que systemd revisa tanto /etc/os-release como /usr/lib/os-release. + # NixOS evidentemente no usa la segunda ruta por ser FHS, así que la duct tape + # final es 'mkdir rootfs/usr/lib && touch rootfs/usr/lib/os-release'. + + config = mkIf cfg.enable { + local = { + mailHost.mdaListen = cfg.hostAddr6; + + nspawn.dmz = { + mtaAddr6 = dmzNet.hosts.mta.v6.address; + hostAddr6 = dmzNet.hosts.gateway.v6.address; + + system = let + containerModule = {...}: { + #TODO: urgente: bloquear puertos de dovecot a non-postfix con iptables + config = { + local = { + preset.dmz = { + enable = true; + container = true; + }; + + mta = { + mdaAddr = "[${mailHost.mdaListen}]"; + mtaListen = cfg.mtaAddr6; + inherit (mailHost) saslPort lmtpPort; + }; + + web.sites = { + home = { + enable = hassEnable; + proxyUrl = "http://[${cfg.hostAddr6}]:${toString hassPort}"; + }; + }; + }; + + nixpkgs = { + pkgs = mkDefault pkgs; + localSystem = mkDefault pkgs.stdenv.hostPlatform; + }; + + services.nginx.virtualHosts = { + "${config.local.domains.imap.main}".locations."^~ /.well-known/acme-challenge/" = { + root = "/var/lib/acme/acme-challenge"; + + extraConfig = '' + auth_basic off; + auth_request off; + ''; + }; + }; + + systemd.network.networks."40-host0" = { + name = "host0"; + + networkConfig = { + DNS = [cfg.dns64]; + + DHCP = "no"; + IPv6AcceptRA = "yes"; + LinkLocalAddressing = "ipv6"; + }; + + ipv6AcceptRAConfig = { + Token = [ + "static:::${dmzNet.hosts.dmz.v6.suffix}" + "eui64" + "static:::${dmzNet.hosts.mta.v6.suffix}" + "static:::${dmzNet.hosts.web.v6.suffix}" + ]; + + UseDNS = false; + }; + }; + }; + }; + in + flakes.trivionomicon.lib.mkSystem { + inherit doctrine flakes pkgs; + + modules = [ + ../. + containerModule + ]; + }; + }; + }; + + services = { + home-assistant.config.http = mkIf hassEnable { + server_host = [cfg.hostAddr6]; + trusted_proxies = [dmzNet.hosts.web.v6.address]; + use_x_forwarded_for = true; + }; + }; + + systemd = { + nspawn.dmz = { + execConfig.PrivateUsers = "pick"; + + filesConfig.BindReadOnly = [ + # idmap porque algunos hacks en nixpkgs (postfix-setup.service) + # asumen que la store es de root + "/nix/store:/nix/store:idmap" + "${cfg.system.config.system.build.toplevel}/init:/sbin/init" + ]; + }; + + network.networks."40-ve-dmz" = { + matchConfig = { + Name = "ve-dmz"; + Driver = "veth"; + }; + + addresses = [ + { + Address = dmzNet.hosts.gateway.v6.cidr; + AddPrefixRoute = "no"; + PreferredLifetime = 0; + } + ]; + + networkConfig = { + LinkLocalAddressing = "ipv6"; + DHCPServer = "no"; + IPMasquerade = "no"; + LLDP = "no"; + EmitLLDP = "no"; + IPv6SendRA = "yes"; + IPv6AcceptRA = "no"; + }; + + ipv6Prefixes = [ + { + Assign = "no"; + Prefix = dmzNet.v6.cidr; + } + ]; + + routes = [ + { + Destination = dmzNet.v6.cidr; + # Sin esto, siempre se escogerá una ULA como source address debido a "PreferredLifetime = 0" en la GUA + PreferredSource = dmzNet.hosts.gateway.v6.address; + } + ]; + }; + + services = { + dovecot.after = ["systemd-nspawn@dmz.service"]; + + "systemd-nspawn@dmz" = { + overrideStrategy = "asDropin"; + + after = ["network-online.target"]; + wants = ["network-online.target"]; + wantedBy = ["machines.target"]; + }; + }; + }; + + networking.firewall = { + allowedTCPPorts = [25 80 443]; + + interfaces.ve-dmz = { + allowedTCPPorts = + [mailHost.saslPort mailHost.lmtpPort] + ++ optional hassEnable hassPort; + + allowedUDPPorts = [67]; # DHCP + }; + }; + }; +} |
