Add `simple-nixos-mailserver` to `umbriel`
Jeremy Fleischman committed
Oct 29, 2024 at 00:20 UTC
c42bed0a3e9a439fbc81bb09e0967beb814bca2e
11 files changed
+363
-3
flake.lock
+85
-1
@@ -1,5 +1,21 @@
1
{
2
"nodes": {
3
+ "blobs": {
4
+ "flake": false,
5
+ "locked": {
6
+ "lastModified": 1604995301,
7
+ "narHash": "sha256-wcLzgLec6SGJA8fx1OEN1yV/Py5b+U5iyYpksUY/yLw=",
8
+ "owner": "simple-nixos-mailserver",
9
+ "repo": "blobs",
10
+ "rev": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265",
11
+ "type": "gitlab"
12
+ },
13
+ "original": {
14
+ "owner": "simple-nixos-mailserver",
15
+ "repo": "blobs",
16
+ "type": "gitlab"
17
+ }
18
+ },
19
"colmena": {
20
"inputs": {
21
"flake-compat": "flake-compat",
@@ -123,6 +139,22 @@
139
"type": "github"
140
}
141
},
142
+ "flake-compat_3": {
143
+ "flake": false,
144
+ "locked": {
145
+ "lastModified": 1696426674,
146
+ "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
147
+ "owner": "edolstra",
148
+ "repo": "flake-compat",
149
+ "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
150
+ "type": "github"
151
+ },
152
+ "original": {
153
+ "owner": "edolstra",
154
+ "repo": "flake-compat",
155
+ "type": "github"
156
+ }
157
+ },
158
"flake-parts": {
159
"inputs": {
160
"nixpkgs-lib": [
@@ -199,6 +231,21 @@
231
"type": "github"
232
}
233
},
234
+ "nixpkgs-24_05": {
235
+ "locked": {
236
+ "lastModified": 1717144377,
237
+ "narHash": "sha256-F/TKWETwB5RaR8owkPPi+SPJh83AQsm6KrQAlJ8v/uA=",
238
+ "owner": "NixOS",
239
+ "repo": "nixpkgs",
240
+ "rev": "805a384895c696f802a9bf5bf4720f37385df547",
241
+ "type": "github"
242
+ },
243
+ "original": {
244
+ "id": "nixpkgs",
245
+ "ref": "nixos-24.05",
246
+ "type": "indirect"
247
+ }
248
+ },
249
"nixpkgs-unstable": {
250
"locked": {
251
"lastModified": 1729265718,
@@ -216,6 +263,21 @@
263
}
264
},
265
"nixpkgs_2": {
266
+ "locked": {
267
+ "lastModified": 1717602782,
268
+ "narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=",
269
+ "owner": "NixOS",
270
+ "repo": "nixpkgs",
271
+ "rev": "e8057b67ebf307f01bdcc8fba94d94f75039d1f6",
272
+ "type": "github"
273
+ },
274
+ "original": {
275
+ "id": "nixpkgs",
276
+ "ref": "nixos-unstable",
277
+ "type": "indirect"
278
+ }
279
+ },
280
+ "nixpkgs_3": {
281
"locked": {
282
"lastModified": 1726871744,
283
"narHash": "sha256-V5LpfdHyQkUF7RfOaDPrZDP+oqz88lTJrMT1+stXNwo=",
@@ -271,11 +333,33 @@
333
"flake-utils": "flake-utils",
334
"nixpkgs": "nixpkgs",
335
"nixpkgs-unstable": "nixpkgs-unstable",
336
+ "simple-nixos-mailserver": "simple-nixos-mailserver",
337
"sops-nix": "sops-nix",
338
"srvos": "srvos",
339
"treefmt-nix": "treefmt-nix"
340
}
341
},
342
+ "simple-nixos-mailserver": {
343
+ "inputs": {
344
+ "blobs": "blobs",
345
+ "flake-compat": "flake-compat_3",
346
+ "nixpkgs": "nixpkgs_2",
347
+ "nixpkgs-24_05": "nixpkgs-24_05"
348
+ },
349
+ "locked": {
350
+ "lastModified": 1722877200,
351
+ "narHash": "sha256-qgKDNJXs+od+1UbRy62uk7dYal3h98I4WojfIqMoGcg=",
352
+ "owner": "simple-nixos-mailserver",
353
+ "repo": "nixos-mailserver",
354
+ "rev": "af7d3bf5daeba3fc28089b015c0dd43f06b176f2",
355
+ "type": "gitlab"
356
+ },
357
+ "original": {
358
+ "owner": "simple-nixos-mailserver",
359
+ "repo": "nixos-mailserver",
360
+ "type": "gitlab"
361
+ }
362
+ },
363
"sops-nix": {
364
"inputs": {
365
"nixpkgs": [
@@ -336,7 +420,7 @@
420
},
421
"treefmt-nix": {
422
"inputs": {
339
- "nixpkgs": "nixpkgs_2"
423
+ "nixpkgs": "nixpkgs_3"
424
},
425
"locked": {
426
"lastModified": 1729242555,
flake.nix
+1
@@ -31,6 +31,7 @@
31
url = "github:numtide/srvos";
32
inputs.nixpkgs.follows = "nixpkgs";
33
};
34
+ simple-nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver";
35
sops-nix = {
36
url = "github:Mic92/sops-nix";
37
inputs = {
non-critical-infra/flake-module.nix
+8
-1
@@ -26,6 +26,7 @@
26
value
27
inputs.disko.nixosModules.disko
28
inputs.first-time-contribution-tagger.nixosModule
29
+ inputs.simple-nixos-mailserver.nixosModule
30
inputs.sops-nix.nixosModules.sops
31
];
32
extraModules = [ inputs.colmena.nixosModules.deploymentOptions ];
@@ -49,8 +50,14 @@
50
};
51
52
perSystem =
52
- { pkgs, inputs', ... }:
53
+ { inputs', ... }:
54
+ # Use the latest packages from `nixpkgs-unstable` for dev tools.
55
+ let
56
+ pkgs = inputs'.nixpkgs-unstable.legacyPackages;
57
+ in
58
{
59
+ packages.encrypt-email-address = pkgs.callPackage ./packages/encrypt-email-address { };
60
+
61
devShells.non-critical-infra = pkgs.mkShellNoCC {
62
packages = [
63
inputs'.colmena.packages.colmena
non-critical-infra/hosts/umbriel.nixos.org/default.nix
+1
@@ -8,6 +8,7 @@
8
../../modules/common.nix
9
../../modules/mjolnir.nix
10
../../modules/prometheus/node-exporter.nix
11
+ ../../modules/mailserver
12
];
13
14
# Bootloader.
non-critical-infra/modules/mailserver/README.md
new
+14
@@ -0,0 +1,14 @@
1
+# NixOS mailserver
2
+
3
+This module will [eventually][issue 485] provide mail services for `nixos.org`.
4
+
5
+## Mailing lists
6
+
7
+To create a new mailing list, or change membership of a mailing list, see the
8
+instructions at the top of [`mailing-lists.nix`](./mailing-lists.nix).
9
+
10
+## Sending mail
11
+
12
+This module does not yet provide SMTP login.
13
+
14
+[issue 485]: https://github.com/NixOS/infra/issues/485
non-critical-infra/modules/mailserver/default.nix
new
+19
@@ -0,0 +1,19 @@
1
+{ config, ... }:
2
+
3
+{
4
+ imports = [ ./mailing-lists.nix ];
5
+
6
+ mailserver = {
7
+ enable = true;
8
+ certificateScheme = "acme-nginx";
9
+
10
+ # Until we have login accounts, there's no reason to run either of these.
11
+ enablePop3 = false;
12
+ enableImap = false;
13
+
14
+ fqdn = config.networking.fqdn;
15
+
16
+ # TODO: change to `nixos.org` when ready
17
+ domains = [ "mail-test.nixos.org" ];
18
+ };
19
+}
non-critical-infra/modules/mailserver/mailing-lists.nix
new
+64
@@ -0,0 +1,64 @@
1
+# This module provides the mailing list definitions for `@nixos.org`.
2
+#
3
+# Simply change the `lists` attribute set below to create new mailing lists or
4
+# edit membership of existing lists.
5
+#
6
+# If you wish to hide your email address, you can encrypt it with SOPS. Just
7
+# run `nix run .#encrypt-email-address -- --help` and follow the instructions.
8
+
9
+{ config, lib, ... }:
10
+
11
+let
12
+ # Mailing lists go here.
13
+ # TODO: replace with the real `nixos.org` mailing lists.
14
+ listsWithSecretFiles = {
15
+ "test-list@mail-test.nixos.org" = [
16
+ "jfly@playground.jflei.com"
17
+ ../../secrets/jfly-email.umbriel
18
+ "jeremyfleischman+subscriber@gmail.com"
19
+ ];
20
+ };
21
+
22
+ fileToSecretId = file: builtins.baseNameOf file;
23
+
24
+ listsWithSecretPlaceholders = lib.mapAttrs' (name: members: {
25
+ name = name;
26
+ value = map (
27
+ member:
28
+ if builtins.isString member then member else config.sops.placeholder.${fileToSecretId member}
29
+ ) members;
30
+ }) listsWithSecretFiles;
31
+
32
+ secretFiles = lib.pipe listsWithSecretFiles [
33
+ (lib.mapAttrsToList (_name: members: members))
34
+ lib.flatten
35
+ (builtins.filter (member: !builtins.isString member))
36
+ ];
37
+in
38
+
39
+{
40
+ # Declare secrets for every secret email in the lists above.
41
+ sops.secrets = builtins.listToAttrs (
42
+ map (file: {
43
+ name = fileToSecretId file;
44
+ value = {
45
+ format = "binary";
46
+ sopsFile = file;
47
+ };
48
+ }) secretFiles
49
+ );
50
+
51
+ # Whenever this changes, we need to manually restart the `postfix-setup`
52
+ # service for postfix to notice the change.
53
+ # TODO: <https://github.com/NixOS/infra/issues/505> tracks fixing this
54
+ sops.templates."postfix-virtual-mailing-lists".content = lib.concatStringsSep "\n" (
55
+ lib.mapAttrsToList (
56
+ name: members: "${name} ${lib.concatStringsSep ", " members}"
57
+ ) listsWithSecretPlaceholders
58
+ );
59
+
60
+ services.postfix.mapFiles.virtual-mailing-lists =
61
+ config.sops.templates."postfix-virtual-mailing-lists".path;
62
+
63
+ services.postfix.config.virtual_alias_maps = [ "hash:/etc/postfix/virtual-mailing-lists" ];
64
+}
non-critical-infra/packages/encrypt-email-address/default.nix
new
+20
@@ -0,0 +1,20 @@
1
+{
2
+ lib,
3
+ python3,
4
+ sops,
5
+}:
6
+
7
+python3.pkgs.buildPythonApplication {
8
+ name = "encrypt-email-address";
9
+ src = ./.;
10
+
11
+ format = "other";
12
+
13
+ propagatedBuildInputs = [ python3.pkgs.click ];
14
+
15
+ installPhase = ''
16
+ mkdir -p $out/bin
17
+ mv ./encrypt-email-address.py $out/bin/encrypt-email-address
18
+ wrapProgram $out/bin/encrypt-email-address --prefix PATH : ${lib.makeBinPath [ sops ]}
19
+ '';
20
+}
non-critical-infra/packages/encrypt-email-address/encrypt-email-address.py
new
+101
@@ -0,0 +1,101 @@
1
+#!/usr/bin/env python3
2
+
3
+import re
4
+import subprocess
5
+from pathlib import Path
6
+
7
+import click
8
+
9
+
10
+def find_project_root(start: Path) -> Path:
11
+ # Can search for `flake.nix` because there are multiple in this project.
12
+ root_indicator = start / ".git/config"
13
+ if root_indicator.exists():
14
+ return start
15
+
16
+ return find_project_root(start.parent)
17
+
18
+
19
+@click.command()
20
+@click.argument("address_id")
21
+@click.argument("email")
22
+@click.option("--force/--no-force", "-f/ ", default=False)
23
+def main(address_id: str, email: str, force: bool) -> None:
24
+ """
25
+ Encrypt an email address (or email addresses) for inclusion in a mailing list.
26
+
27
+ Example:
28
+
29
+ \bencrypt-email-address some-token 'me@example.com,you@example.com'
30
+
31
+ Then follow the instructions for what to do next.
32
+ """
33
+ # Feel free to make the regex less restrictive if you need to.
34
+ id_re = re.compile("[A-Za-z0-9-]+")
35
+ if not id_re.fullmatch(address_id):
36
+ msg = f"Given ID: {address_id!r} is invalid. Must match regex: {id_re.pattern}"
37
+ raise click.ClickException(msg)
38
+
39
+ # Make sure we aren't being given a text file that happens to have a newline at the end.
40
+ clean_email = email.strip()
41
+ if clean_email != email:
42
+ click.secho("Removed whitespace surrounding given email address", fg="yellow")
43
+ email = clean_email
44
+
45
+ project_root = find_project_root(Path.cwd()).relative_to(Path.cwd(), walk_up=True)
46
+ non_critical_infra_dir = project_root / "non-critical-infra"
47
+
48
+ secret_path = non_critical_infra_dir / f"secrets/{address_id}-email.umbriel"
49
+
50
+ if secret_path.exists():
51
+ if not force:
52
+ msg = f"Refusing to clobber existing {secret_path}. Use `--force` to override."
53
+ raise click.ClickException(msg)
54
+ click.secho(f"Clobbering existing {secret_path}", fg="yellow")
55
+
56
+ sops_config = non_critical_infra_dir / ".sops.yaml"
57
+ cp = subprocess.run(
58
+ [
59
+ "sops",
60
+ "--encrypt",
61
+ "--config",
62
+ sops_config,
63
+ "--filename-override",
64
+ secret_path,
65
+ "/dev/stdin",
66
+ ],
67
+ text=True,
68
+ check=True,
69
+ stdout=subprocess.PIPE,
70
+ input=email,
71
+ )
72
+
73
+ secret_path.write_text(cp.stdout)
74
+ subprocess.run(
75
+ ["git", "add", "--intent-to-add", "--force", "--", secret_path], check=True
76
+ )
77
+
78
+ click.secho(f"Successfully generated {secret_path}", fg="green")
79
+
80
+ mailing_list_nix = non_critical_infra_dir / "modules/mailserver/mailing-lists.nix"
81
+ assert mailing_list_nix.exists()
82
+
83
+ click.secho()
84
+ click.secho("Now add yourself to ", nl=False)
85
+ click.secho(mailing_list_nix, fg="blue", nl=False)
86
+ click.secho(". ")
87
+
88
+ click.secho()
89
+ click.secho("Lastly, add `", nl=False)
90
+ click.secho(
91
+ secret_path.relative_to(mailing_list_nix.parent, walk_up=True),
92
+ fg="blue",
93
+ nl=False,
94
+ )
95
+ click.secho("` to the relevant mailing list under '", nl=False)
96
+ click.secho("# Mailing lists go here.", fg="blue", nl=False)
97
+ click.secho("'.")
98
+
99
+
100
+if __name__ == "__main__":
101
+ main()
non-critical-infra/secrets/jfly-email.umbriel
new
+28
@@ -0,0 +1,28 @@
1
+{
2
+ "data": "ENC[AES256_GCM,data:6IohqSwaIUjML5GP886SZ3OQ2NbeuRI1ysrp1wwLe5gP/g==,iv:zPe5s1z8JmoBPP5QgCAEFDptYm+5hJxP1sP+edXW+Bg=,tag:5E9Zg3Mq5CyUqZ/pdvmMDw==,type:str]",
3
+ "sops": {
4
+ "kms": null,
5
+ "gcp_kms": null,
6
+ "azure_kv": null,
7
+ "hc_vault": null,
8
+ "age": [
9
+ {
10
+ "recipient": "age15vcp7875xwtf64j4yshyld0a3hpgzv6n2kxky493s3q0swr9hdaqxugpv6",
11
+ "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUdEFkTk96emFXTStBRHZ1\ndi91RE90Vkk0Z0lFZEdTS0xNdHpGc0lacEZRCnFEWFFKWEtoRWhuZjlldnh3bUhj\nTDZDL1RwMlB6OVZkU1d4dnJjMTNjNDAKLS0tIFB4WjFSWEpCYUdxMkttTTdRakVJ\ndWU5dmVrcnczQTNxRVFrT1l0Ny9hbkUKUyUn7QvmjLAjqAqD8iYAx8ciR8UFhI63\nl8mWy6jmJh2ryMd64+H0B6I+YMBPDHByvG42tlNaO48A2Pe8q0Srug==\n-----END AGE ENCRYPTED FILE-----\n"
12
+ },
13
+ {
14
+ "recipient": "age1j3mkgedmeru63vwww6m44zfw09tg8yw6xdzstaq7ejfkvgcau40qwakm8x",
15
+ "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvNFdxdGlhYzV3WlFLcVJ5\nUWFaNUs3bDFWa3pWOE15VS9abWpvaU80a1VNCmRhemxPanZ1UTdZSDEwU3czU0Fq\nVlM2Y3lOTHQyMFduS2FPMlJ4a1p3OWsKLS0tIEwrbmtNOTRzam0xVndER2ZoQ1N6\nUzM4Qk1BeHBSQVV0dmlKZEhDaC9pbVkKpHjAfJMddyGzo7U7aAl/lqajm8mCwzAq\nPZ/k6MgjER/YAIlDHelvEhPsI8W9VRUYTECRIYMFXkPQaLeQB02OIw==\n-----END AGE ENCRYPTED FILE-----\n"
16
+ },
17
+ {
18
+ "recipient": "age1jrh8yyq3swjru09s75s4mspu0mphh7h6z54z946raa9wx3pcdegq0x8t4h",
19
+ "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKWVF4S3dPanFzbUpWNGtD\ndnJmTUROY2tTWnVCOHdXSXpObUVIbFBMUjJNCjFGRnpTQXJ4eWM0bm9oUVVlaEty\ncXhxUlRXMlVXU3J6dDJpdWlVS2tYcmcKLS0tIGc0N0RmNUtUTHRWMzZVZE5mUklZ\nTmYrbU5yTVo2cTJaOTRKT2pUZzlhUlEKNdlKt3s/fxqKT1V0HwbUf3draDodeBsq\nXnLPalfN2jGHHcMiNj7nweNPy9Nu5l1WvRZ390DkVZlDJ7kkD4lYvg==\n-----END AGE ENCRYPTED FILE-----\n"
20
+ }
21
+ ],
22
+ "lastmodified": "2024-10-29T18:30:30Z",
23
+ "mac": "ENC[AES256_GCM,data:q6lEknFLs1MX4gBtPH8M89lNsAcQR9rWEaG0EobzP0AgUIxUyn9TYvFURD3xMKAp4KcoPfFq7kUO50z11WtC6iw/dAidLhemWOTVT8Vv0SHtc2UKF38MnIDDjmrc05+Hg+X4NFjki76A4NPvIUiIMk+0oL1FK7IMPYBSIBzJqrc=,iv:xl/s2Ah0ccWBDAxHBTXat6hX0yYtYoVbp6gAN2WmZqo=,tag:fXzdg7JaXJdHho4vH0c33Q==,type:str]",
24
+ "pgp": null,
25
+ "unencrypted_suffix": "_unencrypted",
26
+ "version": "3.9.1"
27
+ }
28
+}
\ No newline at end of file
terraform/dns.tf
+22
-1
@@ -330,7 +330,28 @@ locals {
330
hostname = "wiki.nixos.org"
331
type = "AAAA"
332
value = "2a01:4f9:c012:8178::"
333
- }
333
+ },
334
+
335
+ # Mailserver configuration for `mail-test.nixos.org`
336
+ {
337
+ hostname = "mail-test.nixos.org"
338
+ type = "MX"
339
+ value = "umbriel.nixos.org"
340
+ },
341
+ {
342
+ hostname = "mail-test.nixos.org"
343
+ type = "TXT"
344
+ value = "v=spf1 mx -all"
345
+ },
346
+ # TODO: create `DKIM` TXT record: <https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html#set-dkim-signature>.
347
+ # (can't do this until after SNM is deployed: https://github.com/NixOS/infra/pull/495/)
348
+ {
349
+ hostname = "_dmarc.mail-test.nixos.org"
350
+ type = "TXT"
351
+ # TODO: consider making this strict (`v=DMARC1; p=reject; adkim=s; aspf=s;`),
352
+ # but make sure this doesn't break mailing lists: https://dmarcian.com/mailing-lists-dmarc/
353
+ value = "v=DMARC1; p=none"
354
+ },
355
]
356
}
357