Add declarative SNM mail stack on mx1 with DNS-01, DANE, MTA-STS
mx1 runs Simple NixOS Mailserver (Postfix/Dovecot/Rspamd/OpenDKIM) for cnx.email. The TLS cert is obtained via ACME DNS-01 using a dedicated, scoped TSIG key (acme_mx1) that ns1 authorizes for only _acme-challenge.mx1 and _acme-challenge.mta-sts on the cnx.email zone, so the credential can write nothing else. Mailbox passwords are auto-minted by a clan vars generator (four-word passphrase + number). DANE TLSA (3 1 1) is published for _25._tcp.mx1; --reuse-key keeps the key digest stable across renewals. MTA-STS is enforced via a Caddy vhost serving the policy on :443 from the same cert (mta-sts SAN). Firewall opens 25/587/465/143/993/443; 80 stays closed.
This commit is contained in:
Generated
+105
@@ -1,5 +1,21 @@
|
||||
{
|
||||
"nodes": {
|
||||
"blobs": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1604995301,
|
||||
"narHash": "sha256-wcLzgLec6SGJA8fx1OEN1yV/Py5b+U5iyYpksUY/yLw=",
|
||||
"owner": "simple-nixos-mailserver",
|
||||
"repo": "blobs",
|
||||
"rev": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265",
|
||||
"type": "gitlab"
|
||||
},
|
||||
"original": {
|
||||
"owner": "simple-nixos-mailserver",
|
||||
"repo": "blobs",
|
||||
"type": "gitlab"
|
||||
}
|
||||
},
|
||||
"clan-core": {
|
||||
"inputs": {
|
||||
"data-mesher": "data-mesher",
|
||||
@@ -73,6 +89,22 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1767039857,
|
||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
@@ -94,6 +126,54 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"nixos-mailserver",
|
||||
"flake-compat"
|
||||
],
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixos-mailserver",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1772893680,
|
||||
"narHash": "sha256-JDqZMgxUTCq85ObSaFw0HhE+lvdOre1lx9iI6vYyOEs=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "8baab586afc9c9b57645a734c820e4ac0a604af9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixos-mailserver",
|
||||
"git-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-darwin": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@@ -144,6 +224,30 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixos-mailserver": {
|
||||
"inputs": {
|
||||
"blobs": "blobs",
|
||||
"flake-compat": "flake-compat",
|
||||
"git-hooks": "git-hooks",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773912645,
|
||||
"narHash": "sha256-QHzRqq6gh+t3F/QU9DkP7X63dDDcuIQmaDz12p7ANTg=",
|
||||
"owner": "simple-nixos-mailserver",
|
||||
"repo": "nixos-mailserver",
|
||||
"rev": "25e6dbb8fca3b6e779c5a46fd03bd760b2165bb5",
|
||||
"type": "gitlab"
|
||||
},
|
||||
"original": {
|
||||
"owner": "simple-nixos-mailserver",
|
||||
"ref": "nixos-25.11",
|
||||
"repo": "nixos-mailserver",
|
||||
"type": "gitlab"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1778003029,
|
||||
@@ -163,6 +267,7 @@
|
||||
"root": {
|
||||
"inputs": {
|
||||
"clan-core": "clan-core",
|
||||
"nixos-mailserver": "nixos-mailserver",
|
||||
"nixpkgs": [
|
||||
"clan-core",
|
||||
"nixpkgs"
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
||||
inputs.treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||
inputs.treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
# Simple NixOS Mailserver, pinned to the branch matching clan-core's nixpkgs.
|
||||
inputs.nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-25.11";
|
||||
inputs.nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
outputs =
|
||||
{
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
# No formatter, or reformatting would corrupt them.
|
||||
"*.zone" # Knot zone files
|
||||
"docs/book.toml" # mdBook config; no TOML formatter enabled
|
||||
"flake.lock"
|
||||
".envrc"
|
||||
".gitignore"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{ config, ... }:
|
||||
{ config, inputs, ... }:
|
||||
let
|
||||
hosts = import ../../modules/hosts.nix;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
inputs.nixos-mailserver.nixosModules.default
|
||||
../../modules/mail.nix
|
||||
../../modules/static-ipv6.nix
|
||||
../../modules/monitoring/exporters.nix
|
||||
];
|
||||
@@ -17,7 +19,4 @@ in
|
||||
};
|
||||
|
||||
services.timesyncd.enable = true;
|
||||
|
||||
# Mail host backing the cnx.email MX (mx1.cnx.email -> 5.223.65.38).
|
||||
# SMTP/IMAP services to be configured.
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
domains = import ../../modules/dns/domains.nix;
|
||||
mesh = import ../../modules/mesh-hosts.nix { inherit config lib; };
|
||||
@@ -7,6 +12,7 @@ in
|
||||
{
|
||||
imports = [
|
||||
../../modules/dns/authoritative.nix
|
||||
../../modules/dns/acme-mx1-secret.nix
|
||||
../../modules/static-ipv6.nix
|
||||
../../modules/monitoring/exporters.nix
|
||||
];
|
||||
@@ -33,11 +39,9 @@ in
|
||||
time.timeZone = "Etc/GMT-1"; # UTC+1 (fixed offset, no DST)
|
||||
services.timesyncd.enable = true;
|
||||
|
||||
# ACME DNS-01 (RFC 2136): a dedicated TSIG key, scoped to ns1 only, that an
|
||||
# external ACME client uses to write _acme-challenge TXT records. acl_acme
|
||||
# (referenced by each zone below) limits the key to TXT updates at or under
|
||||
# _acme-challenge.<zone>; Knot then signs the record and transfers it to ns2,
|
||||
# which never needs this key. Retrieve the secret for the client with:
|
||||
# ACME DNS-01 (RFC 2136), general key. A dedicated TSIG key scoped by acl_acme
|
||||
# (referenced by every zone below) to TXT updates at or under _acme-challenge.
|
||||
# Retrieve the client config with:
|
||||
# clan vars get ns1 dns-acme-tsig/acme.conf
|
||||
clan.core.vars.generators.dns-acme-tsig = {
|
||||
files."acme.conf" = {
|
||||
@@ -51,8 +55,28 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
# ACME DNS-01, dedicated mx1 key. A *separate* TSIG key (acme_mx1) that only
|
||||
# mx1 holds, rendered from the shared secret (generator dns-acme-mx1-secret,
|
||||
# imported above). acl_acme_mx1 scopes it to TXT updates at exactly
|
||||
# _acme-challenge.mx1 and _acme-challenge.mta-sts (the mail cert and its
|
||||
# MTA-STS SAN), and it is attached only to the cnx.email zone below — so this
|
||||
# credential can write nothing but mx1's own cert challenges.
|
||||
clan.core.vars.generators.dns-acme-mx1-knot = {
|
||||
files."acme.conf" = {
|
||||
secret = true;
|
||||
owner = "knot";
|
||||
group = "knot";
|
||||
};
|
||||
dependencies = [ "dns-acme-mx1-secret" ];
|
||||
script = ''
|
||||
printf 'key:\n - id: acme_mx1\n algorithm: hmac-sha256\n secret: %s\n' \
|
||||
"$(cat "$in"/dns-acme-mx1-secret/secret)" > "$out"/acme.conf
|
||||
'';
|
||||
};
|
||||
|
||||
services.knot.keyFiles = [
|
||||
config.clan.core.vars.generators.dns-acme-tsig.files."acme.conf".path
|
||||
config.clan.core.vars.generators.dns-acme-mx1-knot.files."acme.conf".path
|
||||
];
|
||||
|
||||
services.knot.settings.acl = [
|
||||
@@ -65,6 +89,18 @@ in
|
||||
"update-owner-match" = "sub-or-equal";
|
||||
"update-owner-name" = [ "_acme-challenge" ];
|
||||
}
|
||||
{
|
||||
id = "acl_acme_mx1";
|
||||
key = "acme_mx1";
|
||||
action = [ "update" ];
|
||||
"update-type" = [ "TXT" ];
|
||||
"update-owner" = "name";
|
||||
"update-owner-match" = "sub-or-equal";
|
||||
"update-owner-name" = [
|
||||
"_acme-challenge.mx1"
|
||||
"_acme-challenge.mta-sts"
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
# Automatic DNSSEC signing policy (primary only). ECDSA P-256/SHA-256 with
|
||||
@@ -93,9 +129,12 @@ in
|
||||
"dnssec-signing" = true;
|
||||
"dnssec-policy" = "cnx";
|
||||
notify = [ "ns2" ];
|
||||
# ns2 transfers; acme_ddns does general DNS-01 updates. The dedicated
|
||||
# acme_mx1 key is attached only to cnx.email, so it can't touch other zones.
|
||||
acl = [
|
||||
"acl_ns2"
|
||||
"acl_acme"
|
||||
]; # ns2 transfers; acme_ddns key does DNS-01 updates
|
||||
]
|
||||
++ lib.optionals (d == "cnx.email") [ "acl_acme_mx1" ];
|
||||
}) domains;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Shared TSIG secret for the dedicated acme_mx1 key.
|
||||
#
|
||||
# This key lets mx1 — and only mx1 — write _acme-challenge.mx1.cnx.email TXT
|
||||
# records on ns1 to obtain its mail TLS cert via ACME DNS-01. ns1 scopes it with
|
||||
# acl_acme_mx1 (attached only to the cnx.email zone) so the credential can touch
|
||||
# nothing else. ns1 renders this secret into a Knot key file; mx1 into a lego
|
||||
# rfc2136 env file; both must carry the same secret, hence one shared generator
|
||||
# with a per-host renderer that depends on it. Imported by ns1 and (via mail.nix)
|
||||
# mx1.
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
clan.core.vars.generators.dns-acme-mx1-secret = {
|
||||
share = true;
|
||||
files."secret".secret = true;
|
||||
runtimeInputs = [ pkgs.openssl ];
|
||||
# 32 random bytes, base64 — a valid hmac-sha256 TSIG secret.
|
||||
script = ''openssl rand -base64 32 | tr -d '\n' > "$out"/secret'';
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,27 @@ $TTL 3600
|
||||
|
||||
; ---- Mail ----
|
||||
mx1 IN A 5.223.65.38
|
||||
mx1 IN AAAA 2a01:4ff:2f0:1963::1
|
||||
@ IN MX 10 mx1.cnx.email.
|
||||
@ IN TXT "v=spf1 mx -all"
|
||||
_dmarc IN TXT "v=DMARC1; p=quarantine; rua=mailto:postmaster@cnx.email"
|
||||
|
||||
; ---- DANE / TLSA ----
|
||||
; "3 1 1" = DANE-EE, SPKI, SHA-256: the digest of mx1's certificate public key.
|
||||
; Valid because the zone is DNSSEC-signed and the lego cert uses --reuse-key, so
|
||||
; the key (and thus this digest) is stable across renewals. Compute it AFTER the
|
||||
; first issuance and paste the hex below:
|
||||
; ssh mx1 'openssl x509 -in /var/lib/acme/mx1.cnx.email/cert.pem -noout -pubkey \
|
||||
; | openssl pkey -pubin -outform DER | openssl dgst -sha256 -binary | xxd -p -c256'
|
||||
_25._tcp.mx1 IN TLSA 3 1 1 bd9a51f60b6d2dd20f18b3553d2795053ac52f87567a46bc892006bb58506404
|
||||
|
||||
; ---- MTA-STS ----
|
||||
; Policy host (A/AAAA point at mx1); the _mta-sts TXT id MUST be bumped whenever
|
||||
; the policy file in modules/mail.nix changes, or senders keep the cached policy.
|
||||
mta-sts IN A 5.223.65.38
|
||||
mta-sts IN AAAA 2a01:4ff:2f0:1963::1
|
||||
_mta-sts IN TXT "v=STSv1; id=2026061801"
|
||||
mail._domainkey IN TXT ( "v=DKIM1; k=rsa; "
|
||||
"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr9QxTs5dLtY76bo156+Tp0GUoE554rMwIooIYa2MMYHNs8zPb0thFmaCKGAINdHKNIq2phXAlk51iBTfdqXjx7gVWSrs+ftykqO3b5hUjgImsgqPWGUTzy5/bUgcDELiD9KKEyKYD3+ebZEw6d0uvBvEsA6a1CPzOsufoCDtyKjByCuQzkCBrK25TUHFolGvEYcZexR0LSF+8hMss"
|
||||
"xyw9NYiPpTXVCWQJnrZZpuOBiX0K2l5CAXVyuT/B5RcBXlAUhBTp3390VEhL0wAZMTOnvtvBYK3NnsTIh96fkh6MfWmre7Fi9hEq//xGf40N5/aomMjJrJdqFZJLZpDotb/XwIDAQAB"
|
||||
)
|
||||
|
||||
@@ -24,16 +24,25 @@ let
|
||||
description = "ICMP (ping / PMTUD)";
|
||||
};
|
||||
|
||||
# Inbound mail only. mx1 is the MX for cnx.email, so other servers deliver on
|
||||
# 25. Submission (587/465) and IMAP (993) stay closed until the mail stack and
|
||||
# mailboxes exist — admin access rides the mesh, same as the other hosts.
|
||||
smtp = {
|
||||
# Public mail ports for mx1 (MX for cnx.email). 25 is server-to-server
|
||||
# delivery; 587/465 are client submission; 143/993 are IMAP. 443 serves only the
|
||||
# MTA-STS policy (https://mta-sts.cnx.email/.well-known/mta-sts.txt); the cert
|
||||
# itself uses ACME DNS-01 so port 80 stays closed. Admin still rides the mesh.
|
||||
mailPort = port: description: {
|
||||
direction = "in";
|
||||
protocol = "tcp";
|
||||
port = "25";
|
||||
inherit port;
|
||||
source_ips = world;
|
||||
description = "SMTP (inbound mail)";
|
||||
inherit description;
|
||||
};
|
||||
mailRules = [
|
||||
(mailPort "25" "SMTP (inbound mail)")
|
||||
(mailPort "587" "Submission (STARTTLS)")
|
||||
(mailPort "465" "Submission (implicit TLS)")
|
||||
(mailPort "143" "IMAP (STARTTLS)")
|
||||
(mailPort "993" "IMAP (implicit TLS)")
|
||||
(mailPort "443" "MTA-STS policy (HTTPS)")
|
||||
];
|
||||
|
||||
dnsRules = [
|
||||
{
|
||||
@@ -61,8 +70,7 @@ in
|
||||
];
|
||||
"clan-ns1" = dnsRules;
|
||||
"clan-ns2" = dnsRules;
|
||||
"clan-mx1" = [
|
||||
smtp
|
||||
"clan-mx1" = mailRules ++ [
|
||||
zerotier
|
||||
ping
|
||||
];
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
# Declarative mail stack for mx1 (Simple NixOS Mailserver: Postfix + Dovecot +
|
||||
# Rspamd + OpenDKIM). Imported by machines/mx1 alongside the SNM flake module.
|
||||
#
|
||||
# Mailboxes are virtual (not system users): each address below is a login account
|
||||
# whose password is auto-generated by a clan vars generator as a four-word
|
||||
# passphrase with a trailing number (e.g. otter-lantern-cobalt-driftwood-42). The
|
||||
# generator stores both the passphrase and its sha-512 hash. To add a mailbox:
|
||||
# append the address to `accounts`, run `clan vars generate mx1`, redeploy mx1,
|
||||
# then hand the passphrase to the user:
|
||||
# clan vars get mx1 mail-passwd-<addr>/passphrase
|
||||
# (addr with @ and . replaced by -at- and -, e.g. mail-passwd-postmaster-at-cnx-email)
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
hosts = import ./hosts.nix;
|
||||
fqdn = "mx1.cnx.email";
|
||||
mtaStsHost = "mta-sts.cnx.email";
|
||||
|
||||
# MTA-STS policy served at https://mta-sts.cnx.email/.well-known/mta-sts.txt.
|
||||
# enforce = a sending MTA that fetched this must use a valid, MX-matching TLS
|
||||
# cert or refuse to deliver. Bump the _mta-sts TXT id (in the zone) whenever
|
||||
# this changes.
|
||||
mtaStsPolicy = pkgs.writeText "mta-sts.txt" ''
|
||||
version: STSv1
|
||||
mode: enforce
|
||||
mx: ${fqdn}
|
||||
max_age: 604800
|
||||
'';
|
||||
|
||||
# The mailboxes mx1 serves. postmaster is required by RFC 5321.
|
||||
accounts = [
|
||||
"postmaster@cnx.email"
|
||||
];
|
||||
|
||||
genName = addr: "mail-passwd-" + lib.replaceStrings [ "@" "." ] [ "-at-" "-" ] addr;
|
||||
|
||||
passwdGenerators = lib.listToAttrs (
|
||||
map (addr: {
|
||||
name = genName addr;
|
||||
value = {
|
||||
files."passphrase".secret = true; # retrievable to hand to the user
|
||||
files."hash".secret = true; # consumed by SNM's hashedPasswordFile
|
||||
runtimeInputs = [
|
||||
pkgs.xkcdpass
|
||||
pkgs.mkpasswd
|
||||
];
|
||||
script = ''
|
||||
pass="$(xkcdpass --numwords=4 --delimiter=- --case=lower)-$((RANDOM % 90 + 10))"
|
||||
printf '%s' "$pass" > "$out"/passphrase
|
||||
printf '%s' "$pass" | mkpasswd -s -m sha-512 > "$out"/hash
|
||||
'';
|
||||
};
|
||||
}) accounts
|
||||
);
|
||||
|
||||
loginAccounts = lib.listToAttrs (
|
||||
map (addr: {
|
||||
name = addr;
|
||||
value.hashedPasswordFile = config.clan.core.vars.generators.${genName addr}.files."hash".path;
|
||||
}) accounts
|
||||
);
|
||||
in
|
||||
{
|
||||
imports = [ ./dns/acme-mx1-secret.nix ];
|
||||
|
||||
clan.core.vars.generators = passwdGenerators // {
|
||||
# Render the shared acme_mx1 TSIG secret into a lego rfc2136 env file. lego
|
||||
# (via security.acme below) uses it to write the _acme-challenge.mx1.cnx.email
|
||||
# TXT record to ns1, which authorizes the acme_mx1 key for exactly that owner.
|
||||
dns-acme-rfc2136 = {
|
||||
files."rfc2136.env".secret = true; # root-owned; systemd reads it as root
|
||||
dependencies = [ "dns-acme-mx1-secret" ];
|
||||
script = ''
|
||||
printf 'RFC2136_NAMESERVER=${hosts.ns1.ipv4}:53\nRFC2136_TSIG_ALGORITHM=hmac-sha256.\nRFC2136_TSIG_KEY=acme_mx1\nRFC2136_TSIG_SECRET=%s\n' \
|
||||
"$(cat "$in"/dns-acme-mx1-secret/secret)" > "$out"/rfc2136.env
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
mailserver = {
|
||||
enable = true;
|
||||
# Fresh install: declare the latest layout the nixos-25.11 branch ships (3),
|
||||
# so SNM uses the current dovecot mail directory layout with nothing to migrate.
|
||||
stateVersion = 3;
|
||||
inherit fqdn;
|
||||
domains = [ "cnx.email" ];
|
||||
inherit loginAccounts;
|
||||
|
||||
# Consume a security.acme cert we obtain ourselves via DNS-01 (below); no
|
||||
# web server and no inbound HTTP needed, so port 80 stays closed. Add the
|
||||
# MTA-STS host as a SAN so the one cert also covers the policy endpoint.
|
||||
certificateScheme = "acme";
|
||||
certificateDomains = [ mtaStsHost ];
|
||||
|
||||
dkimSelector = "mail";
|
||||
};
|
||||
|
||||
security.acme = {
|
||||
acceptTerms = true;
|
||||
defaults.email = "postmaster@cnx.email";
|
||||
certs.${fqdn} = {
|
||||
dnsProvider = "rfc2136";
|
||||
environmentFile = config.clan.core.vars.generators.dns-acme-rfc2136.files."rfc2136.env".path;
|
||||
# ns1 is the only nameserver that accepts the acme_mx1 UPDATE; check
|
||||
# propagation against it directly rather than a public resolver.
|
||||
dnsResolver = "${hosts.ns1.ipv4}:53";
|
||||
# Keep the private key fixed across renewals so the DANE TLSA "3 1 1"
|
||||
# record (public-key digest, published in the zone) stays valid.
|
||||
extraLegoRenewFlags = [ "--reuse-key" ];
|
||||
};
|
||||
};
|
||||
|
||||
# The mail cert is owned group=acme (SNM adds postfix/dovecot); Caddy serves the
|
||||
# MTA-STS endpoint from the same cert, so it needs to read the key too.
|
||||
users.users.caddy.extraGroups = [ "acme" ];
|
||||
|
||||
# MTA-STS policy endpoint, served by Caddy (same web server as control's docs).
|
||||
# The explicit `tls cert key` points at the lego-issued mail cert (which carries
|
||||
# mta-sts.cnx.email as a SAN) and disables Caddy's automatic ACME, so no extra
|
||||
# issuance happens and the DANE TLSA key stays stable. Only :443 is opened.
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
virtualHosts.${mtaStsHost}.extraConfig = ''
|
||||
tls /var/lib/acme/${fqdn}/cert.pem /var/lib/acme/${fqdn}/key.pem
|
||||
root * ${pkgs.writeTextDir ".well-known/mta-sts.txt" (builtins.readFile mtaStsPolicy)}
|
||||
file_server
|
||||
'';
|
||||
};
|
||||
|
||||
# DKIM private keys are generated on first start under this dir. They're
|
||||
# regenerable (rotate + republish the TXT), but declaring the path as clan
|
||||
# state lets a borg client back it up to avoid a needless DNS round-trip on
|
||||
# restore. Wiring mx1 into the borgbackup instance is a separate step.
|
||||
clan.core.state.mail-dkim.folders = [ config.mailserver.dkimKeyDirectory ];
|
||||
}
|
||||
Reference in New Issue
Block a user