# Restricted DNS-01 certificate issuance for ci.commoninternet.net. # # This host is authoritative only for acme.commoninternet.net. Gandi continues # to own commoninternet.net; it delegates this narrow zone and one permanent # _acme-challenge CNAME manually. No Gandi credential is present here. { pkgs, ... }: let acmeDnsConfig = pkgs.writeText "cc-ci-acme-dns.conf" '' [general] listen = "91.98.47.73:53" protocol = "both4" domain = "acme.commoninternet.net" nsname = "ns-acme.commoninternet.net" nsadmin = "hostmaster.commoninternet.net" records = [ "acme.commoninternet.net. NS ns-acme.commoninternet.net.", "ns-acme.commoninternet.net. A 91.98.47.73", ] debug = false [database] # acme-dns 2.x registers the embedded driver under `sqlite` (not the # legacy `sqlite3` identifier). engine = "sqlite" connection = "/var/lib/acme-dns/acme-dns.db" [api] ip = "127.0.0.1" port = "8080" tls = "none" # The one Lego account was bootstrapped before this configuration was # hardened. Updates authenticated by that account remain available. disable_registration = true corsorigins = [] [logconfig] loglevel = "info" logtype = "stdout" logformat = "json" ''; # These are wiring values only. The acme-dns account JSON is generated by # Lego below /var/lib/acme and never enters Nix, git, or /etc. legoEnvironment = pkgs.writeText "cc-ci-acme-dns-lego.env" '' ACME_DNS_API_BASE=http://127.0.0.1:8080 ACME_DNS_STORAGE_PATH=/var/lib/acme/ci.commoninternet.net/acme-dns-accounts.json ACME_DNS_ALLOWLIST=127.0.0.1/32 ''; in { users.groups.acme-dns = { }; users.users.acme-dns = { isSystemUser = true; group = "acme-dns"; home = "/var/lib/acme-dns"; }; environment.etc."acme-dns/lego.env".source = legoEnvironment; # The staging order has completed successfully. This marker permits the # production ACME post-run hook to hand a renewed certificate to Traefik. systemd.tmpfiles.rules = [ "f /var/lib/ci-certs/acme-production-enabled 0600 root root -" ]; networking.firewall = { allowedTCPPorts = [ 53 ]; allowedUDPPorts = [ 53 ]; }; systemd.services.acme-dns = { description = "Restricted authoritative DNS for cc-ci ACME DNS-01"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; serviceConfig = { User = "acme-dns"; Group = "acme-dns"; StateDirectory = "acme-dns"; StateDirectoryMode = "0700"; WorkingDirectory = "/var/lib/acme-dns"; ExecStart = "${pkgs.acme-dns}/bin/acme-dns -c ${acmeDnsConfig}"; Restart = "on-failure"; RestartSec = "5s"; AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; NoNewPrivileges = true; PrivateTmp = true; PrivateDevices = true; ProtectHome = true; ProtectSystem = "strict"; ReadWritePaths = [ "/var/lib/acme-dns" ]; RestrictAddressFamilies = [ "AF_INET" "AF_UNIX" ]; }; }; # Traefik consumes its wildcard as immutable Swarm secrets, so a renewed # host certificate must be copied and reconciled rather than merely reloaded. # This service is started only by the production-mode ACME postRun hook. systemd.services.cc-ci-acme-traefik-handoff = { description = "Install renewed cc-ci wildcard into Traefik Swarm secrets"; after = [ "docker.service" "deploy-proxy.service" ]; requires = [ "docker.service" ]; path = [ pkgs.coreutils pkgs.docker pkgs.systemd pkgs.gnugrep ]; serviceConfig = { Type = "oneshot"; UMask = "0077"; }; script = '' src=/var/lib/acme/ci.commoninternet.net dst=/var/lib/ci-certs/live test -s "$src/fullchain.pem" test -s "$src/key.pem" install -d -m 0700 "$dst" install -m 0444 "$src/fullchain.pem" "$dst/fullchain.pem.new" install -m 0400 "$src/key.pem" "$dst/privkey.pem.new" mv -f "$dst/fullchain.pem.new" "$dst/fullchain.pem" mv -f "$dst/privkey.pem.new" "$dst/privkey.pem" # deploy-proxy performs the health-gated Swarm rollout. Its reconciler # derives a fresh version from the public certificate chain and inserts # the matching ssl_cert/ssl_key secrets before deploying Traefik. systemctl restart deploy-proxy.service # A successful rollout no longer references old wildcard versions. Best # effort removal retains any secret Docker still reports as in use. keep="v$(sha256sum "$dst/fullchain.pem" | cut -c1-16)" docker secret ls --format '{{.Name}}' | \ grep -E '^traefik_ci_commoninternet_net_ssl_(cert|key)_v' | \ grep -v -E "_(ssl_cert|ssl_key)_$keep\$" | \ while IFS= read -r stale; do docker secret rm "$stale" || true; done ''; }; security.acme = { acceptTerms = true; certs."ci.commoninternet.net" = { domain = "ci.commoninternet.net"; extraDomainNames = [ "*.ci.commoninternet.net" ]; # Staging issuance succeeded using the permanent, narrowly delegated # CNAME. Production uses the same restricted acme-dns account. dnsProvider = "acmedns"; environmentFile = "/etc/acme-dns/lego.env"; dnsResolver = "1.1.1.1:53"; server = "https://acme-v02.api.letsencrypt.org/directory"; postRun = '' # The production marker is deployed only after staging proves the # permanent CNAME and restricted acme-dns account work end to end. if [ -e /var/lib/ci-certs/acme-production-enabled ]; then ${pkgs.systemd}/bin/systemctl --no-block start cc-ci-acme-traefik-handoff.service fi ''; }; }; }