From 80ea31cc30deefff157b504fee991c499877722f Mon Sep 17 00:00:00 2001 From: flop Date: Sun, 5 Jul 2026 20:31:52 +0200 Subject: [PATCH] feat: add acme --- flake.nix | 1 + machines/core.domain.com/configuration.nix | 14 +++-- .../hardware-configuration.nix | 2 +- machines/deploy.domain.com/configuration.nix | 13 ++-- .../hardware-configuration.nix | 28 +++++++++ modules/acme.nix | 61 +++++++++++++++++++ modules/nginx.nix | 14 +++-- 7 files changed, 118 insertions(+), 15 deletions(-) create mode 100644 machines/deploy.domain.com/hardware-configuration.nix create mode 100644 modules/acme.nix diff --git a/flake.nix b/flake.nix index 11d2f86..98675b7 100644 --- a/flake.nix +++ b/flake.nix @@ -38,6 +38,7 @@ ./modules/users.nix ./modules/auto-update.nix ./modules/nginx.nix + ./modules/acme.nix ]; # Central subdomain -> service mapping. diff --git a/machines/core.domain.com/configuration.nix b/machines/core.domain.com/configuration.nix index 8c59e50..9c3adf2 100644 --- a/machines/core.domain.com/configuration.nix +++ b/machines/core.domain.com/configuration.nix @@ -3,13 +3,12 @@ { imports = [ ./hardware-configuration.nix - ../../modules/keycloak.nix ]; boot.loader.grub.enable = true; boot.loader.grub.device = "/dev/sda"; - networking.hostName = "auth"; + networking.hostName = "core"; networking.domain = "domain.com"; time.timeZone = "UTC"; @@ -29,6 +28,11 @@ bash ]; + infra.acme = { + enable = true; + email = "admin@${config.networking.domain}"; + }; + services.keycloak = { enable = true; hostname = "auth.domain.com"; @@ -47,10 +51,10 @@ infra.autoUpdate = { enable = true; - # selects the nixosConfigurations.auth.domain.com output from the flake - flake = "${git_url}#auth.domain.com"; + # selects the nixosConfigurations.core.domain.com output from the flake + flake = "${git_url}#core.domain.com"; allowReboot = true; }; system.stateVersion = "26.05"; -} \ No newline at end of file +} diff --git a/machines/core.domain.com/hardware-configuration.nix b/machines/core.domain.com/hardware-configuration.nix index 8d263f4..9ba40c6 100644 --- a/machines/core.domain.com/hardware-configuration.nix +++ b/machines/core.domain.com/hardware-configuration.nix @@ -25,4 +25,4 @@ networking.useDHCP = lib.mkDefault true; nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; -} \ No newline at end of file +} diff --git a/machines/deploy.domain.com/configuration.nix b/machines/deploy.domain.com/configuration.nix index e3ac073..b975c55 100644 --- a/machines/deploy.domain.com/configuration.nix +++ b/machines/deploy.domain.com/configuration.nix @@ -8,7 +8,7 @@ boot.loader.grub.enable = true; boot.loader.grub.device = "/dev/sda"; - networking.hostName = "status"; + networking.hostName = "deploy"; networking.domain = "domain.com"; time.timeZone = "UTC"; @@ -28,6 +28,11 @@ bash ]; + infra.acme = { + enable = true; + email = "admin@${config.networking.domain}"; + }; + infra.gitea = { enable = true; hostname = "git.${config.networking.domain}"; @@ -40,10 +45,10 @@ infra.autoUpdate = { enable = true; - # selects the nixosConfigurations.status.domain.com output from the flake - flake = "${git_url}#status.domain.com"; + # selects the nixosConfigurations.deploy.domain.com output from the flake + flake = "${git_url}#deploy.domain.com"; allowReboot = true; }; system.stateVersion = "26.05"; -} \ No newline at end of file +} diff --git a/machines/deploy.domain.com/hardware-configuration.nix b/machines/deploy.domain.com/hardware-configuration.nix new file mode 100644 index 0000000..9ba40c6 --- /dev/null +++ b/machines/deploy.domain.com/hardware-configuration.nix @@ -0,0 +1,28 @@ +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ "xhci_pci" "ehci_pci" "ahci" "usb_storage" "sd_mod" "sr_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-label/nixos"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-label/boot"; + fsType = "vfat"; + }; + + swapDevices = [ { device = "/dev/disk/by-label/swap"; } ]; + + networking.useDHCP = lib.mkDefault true; + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/modules/acme.nix b/modules/acme.nix new file mode 100644 index 0000000..28d79cf --- /dev/null +++ b/modules/acme.nix @@ -0,0 +1,61 @@ +# Let's Encrypt (ACME) certificate issuance, per-host HTTP-01. +# +# Termination & deployment scheme +# ------------------------------- +# Each NixOS host terminates TLS itself for the subdomains it serves. +# There is no central certificate store and no distribution step. +# +# 1. A host declares which subdomains it serves via `domains = [ ... ]` +# in flake.nix (consumed by modules/nginx.nix). +# 2. With `infra.acme.enable = true`, the nginx module sets +# `enableACME = true; forceSSL = true;` on every served vhost. +# 3. security.acme requests one cert per vhost via the HTTP-01 +# challenge; nginx answers /.well-known/acme-challenge/ on :80. +# 4. On issue/renewal, nginx is reloaded automatically +# (`reloadServices`), so the new certificate goes live at once. +# 5. Renewals run on a systemd timer; every host (core, deploy, ...) +# keeps its own certificates fresh — nothing is deployed by hand. +# +# Adding a new endpoint (e.g. deploy.domain.com): +# - add the host (or add the subdomain to an existing host's `domains`), +# - enable `infra.acme` on that host, +# - point the subdomain's DNS at the host and leave :80 reachable so +# the HTTP-01 challenge can complete. + +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.infra.acme; +in { + options.infra.acme = { + enable = mkEnableOption "Let's Encrypt (ACME) TLS certificates"; + + email = mkOption { + type = types.str; + description = "Account email used for Let's Encrypt"; + }; + + staging = mkOption { + type = types.bool; + default = false; + description = '' + Use the Let's Encrypt staging environment + (untrusted certificates, no rate limiting) for testing. + ''; + }; + }; + + config = mkIf cfg.enable { + security.acme = { + acceptTerms = true; + defaults = { + inherit (cfg) email; + reloadServices = [ "nginx.service" ]; + } // optionalAttrs cfg.staging { + server = "https://acme-staging-v02.api.letsencrypt.org/directory"; + }; + }; + }; +} diff --git a/modules/nginx.nix b/modules/nginx.nix index 4340f85..653c262 100644 --- a/modules/nginx.nix +++ b/modules/nginx.nix @@ -3,18 +3,22 @@ let served = lib.filterAttrs (k: _: builtins.elem k domains) vhosts; enabled = served != { }; + acme = config.infra.acme.enable or false; in { config = lib.mkIf enabled { services.nginx = { enable = true; virtualHosts = lib.mapAttrs (name: vhost: let - port = vhost.ports.http or 80; + port = vhost.ports.http or null; + hasProxy = port != null; + root = vhost.root or null; in { - enableACME = vhost.enableACME or false; - forceSSL = vhost.enableACME or false; + enableACME = acme; + forceSSL = acme; + root = lib.mkIf (root != null) root; extraConfig = vhost.extraConfig or ""; - locations."/" = { + locations."/" = lib.mkIf hasProxy { proxyPass = "http://localhost:${toString port}"; proxyWebsockets = vhost.websockets or false; }; @@ -24,4 +28,4 @@ in { networking.firewall.allowedTCPPorts = [ 80 443 ]; }; -} \ No newline at end of file +}