147 lines
5.2 KiB
Nix
147 lines
5.2 KiB
Nix
# 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.",
|
|
]
|
|
debug = false
|
|
|
|
[database]
|
|
engine = "sqlite3"
|
|
connection = "/var/lib/acme-dns/acme-dns.db"
|
|
|
|
[api]
|
|
ip = "127.0.0.1"
|
|
port = "8080"
|
|
tls = "none"
|
|
# Bootstrap registration is deliberately temporary. Once the single Lego
|
|
# account exists, change this to true in a follow-up reviewed deployment.
|
|
disable_registration = false
|
|
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;
|
|
|
|
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" ];
|
|
# The pinned Lego provider spells this `acmedns`; keep the service on
|
|
# staging until the operator has installed the permanent CNAME.
|
|
dnsProvider = "acmedns";
|
|
environmentFile = "/etc/acme-dns/lego.env";
|
|
dnsResolver = "1.1.1.1:53";
|
|
server = "https://acme-staging-v02.api.letsencrypt.org/directory";
|
|
postRun = ''
|
|
# Production cutover creates this marker in a separate reviewed
|
|
# deployment. Staging issuance must never replace the live cert.
|
|
if [ -e /var/lib/ci-certs/acme-production-enabled ]; then
|
|
${pkgs.systemd}/bin/systemctl --no-block start cc-ci-acme-traefik-handoff.service
|
|
fi
|
|
'';
|
|
};
|
|
};
|
|
}
|