@cryptotaxi247 / infra-1 / commits / c42bed0a

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