1{ pkgs, lib, ... }:
2let
3 orga = "example";
4 domain = "${orga}.localdomain";
5
6 tls-cert = pkgs.runCommand "selfSignedCert" { buildInputs = [ pkgs.openssl ]; } ''
7 openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -days 36500 \
8 -subj '/CN=machine.${domain}'
9 install -D -t $out key.pem cert.pem
10 '';
11
12 gitRepositories = [
13 "repo1"
14 "repo2"
15 ];
16in
17{
18 name = "public-inbox";
19
20 meta.maintainers = with pkgs.lib.maintainers; [ julm ];
21
22 nodes.machine =
23 {
24 config,
25 pkgs,
26 nodes,
27 ...
28 }:
29 let
30 inherit (config.services) public-inbox;
31 in
32 {
33 virtualisation.diskSize = 1 * 1024;
34 virtualisation.memorySize = 1 * 1024;
35 networking.domain = domain;
36
37 security.pki.certificateFiles = [ "${tls-cert}/cert.pem" ];
38 # If using security.acme:
39 #security.acme.certs."${domain}".postRun = ''
40 # systemctl try-restart public-inbox-nntpd public-inbox-imapd
41 #'';
42
43 services.public-inbox = {
44 enable = true;
45 postfix.enable = true;
46 openFirewall = true;
47 settings.publicinbox = {
48 css = [ "href=https://machine.${domain}/style/light.css" ];
49 nntpserver = [ "nntps://machine.${domain}" ];
50 wwwlisting = "match=domain";
51 };
52 mda = {
53 enable = true;
54 args = [ "--no-precheck" ]; # Allow Bcc:
55 };
56 http = {
57 enable = true;
58 port = "/run/public-inbox-http.sock";
59 #port = 8080;
60 args = [ "-W0" ];
61 mounts = [
62 "https://machine.${domain}/inbox"
63 ];
64 };
65 nntp = {
66 enable = true;
67 #port = 563;
68 args = [ "-W0" ];
69 cert = "${tls-cert}/cert.pem";
70 key = "${tls-cert}/key.pem";
71 };
72 imap = {
73 enable = true;
74 #port = 993;
75 args = [ "-W0" ];
76 cert = "${tls-cert}/cert.pem";
77 key = "${tls-cert}/key.pem";
78 };
79 inboxes =
80 lib.recursiveUpdate
81 (lib.genAttrs gitRepositories (repo: {
82 address = [
83 # Routed to the "public-inbox:" transport in services.postfix.transport
84 "${repo}@${domain}"
85 ];
86 description = ''
87 ${repo}@${domain} :
88 discussions about ${repo}.
89 '';
90 url = "https://machine.${domain}/inbox/${repo}";
91 newsgroup = "inbox.comp.${orga}.${repo}";
92 coderepo = [ repo ];
93 }))
94 {
95 repo2 = {
96 hide = [
97 "imap" # FIXME: doesn't work for IMAP as of public-inbox 1.6.1
98 "manifest"
99 "www"
100 ];
101 };
102 };
103 settings.coderepo = lib.listToAttrs (
104 map (
105 repositoryName:
106 lib.nameValuePair repositoryName {
107 dir = "/var/lib/public-inbox/repositories/${repositoryName}.git";
108 cgitUrl = "https://git.${domain}/${repositoryName}.git";
109 }
110 ) gitRepositories
111 );
112 };
113
114 # Use nginx as a reverse proxy for public-inbox-httpd
115 services.nginx = {
116 enable = true;
117 recommendedGzipSettings = true;
118 recommendedOptimisation = true;
119 recommendedTlsSettings = true;
120 recommendedProxySettings = true;
121 virtualHosts."machine.${domain}" = {
122 forceSSL = true;
123 sslCertificate = "${tls-cert}/cert.pem";
124 sslCertificateKey = "${tls-cert}/key.pem";
125 locations."/".return = "302 /inbox";
126 locations."= /inbox".return = "302 /inbox/";
127 locations."/inbox".proxyPass = "http://unix:${public-inbox.http.port}:/inbox";
128 # If using TCP instead of a Unix socket:
129 #locations."/inbox".proxyPass = "http://127.0.0.1:${toString public-inbox.http.port}/inbox";
130 # Referred to by settings.publicinbox.css
131 # See http://public-inbox.org/meta/_/text/color/
132 locations."= /style/light.css".alias = pkgs.writeText "light.css" ''
133 * { background:#fff; color:#000 }
134
135 a { color:#00f; text-decoration:none }
136 a:visited { color:#808 }
137
138 *.q { color:#008 }
139
140 *.add { color:#060 }
141 *.del {color:#900 }
142 *.head { color:#000 }
143 *.hunk { color:#960 }
144
145 .hl.num { color:#f30 } /* number */
146 .hl.esc { color:#f0f } /* escape character */
147 .hl.str { color:#f30 } /* string */
148 .hl.ppc { color:#c3c } /* preprocessor */
149 .hl.pps { color:#f30 } /* preprocessor string */
150 .hl.slc { color:#099 } /* single-line comment */
151 .hl.com { color:#099 } /* multi-line comment */
152 /* .hl.opt { color:#ccc } */ /* operator */
153 /* .hl.ipl { color:#ccc } */ /* interpolation */
154
155 /* keyword groups kw[a-z] */
156 .hl.kwa { color:#f90 }
157 .hl.kwb { color:#060 }
158 .hl.kwc { color:#f90 }
159 /* .hl.kwd { color:#ccc } */
160 '';
161 };
162 };
163
164 services.postfix = {
165 enable = true;
166 setSendmail = true;
167 #sslCert = "${tls-cert}/cert.pem";
168 #sslKey = "${tls-cert}/key.pem";
169 settings.main.recipient_delimiter = "+";
170 };
171
172 environment.systemPackages = [
173 pkgs.gitMinimal
174 pkgs.mailutils
175 pkgs.openssl
176 ];
177
178 };
179
180 testScript = ''
181 start_all()
182
183 # The threshold and/or hardening may have to be changed with new features/checks
184 with subtest("systemd hardening thresholds"):
185 print(machine.succeed("systemd-analyze security public-inbox-httpd.service --threshold=5 --no-pager"))
186 print(machine.succeed("systemd-analyze security public-inbox-imapd.service --threshold=5 --no-pager"))
187 print(machine.succeed("systemd-analyze security public-inbox-nntpd.service --threshold=4 --no-pager"))
188
189 machine.wait_for_unit("multi-user.target")
190 machine.wait_for_unit("public-inbox-init.service")
191
192 machine.succeed(
193 ${lib.concatMapStrings (repositoryName: ''
194 "sudo -u public-inbox git init --bare -b main /var/lib/public-inbox/repositories/${repositoryName}.git",
195 '') gitRepositories}
196 )
197
198 # List inboxes through public-inbox-httpd
199 machine.wait_for_unit("public-inbox-httpd.socket")
200 machine.wait_for_unit("nginx.service")
201 machine.succeed("curl -L https://machine.${domain} | grep repo1@${domain}")
202 # The repo2 inbox is hidden
203 machine.fail("curl -L https://machine.${domain} | grep repo2@${domain}")
204
205 # Send a mail and read it through public-inbox-httpd
206 # Must work too when using a recipientDelimiter.
207 machine.wait_for_unit("postfix.service")
208 machine.succeed("mail -t <${pkgs.writeText "mail" ''
209 Subject: Testing mail
210 From: root@localhost
211 To: repo1+extension@${domain}
212 Message-ID: <repo1@root-1>
213 Content-Type: text/plain; charset=utf-8
214 Content-Disposition: inline
215
216 This is a testing mail.
217 ''}")
218 machine.sleep(10)
219 machine.succeed("curl -L 'https://machine.${domain}/inbox/repo1/repo1@root-1/T/#u' | grep 'This is a testing mail.'")
220
221 # Read a mail through public-inbox-imapd
222 machine.wait_for_unit("public-inbox-imapd.socket")
223 machine.succeed("openssl s_client -ign_eof -crlf -connect machine.${domain}:993 <${pkgs.writeText "imap-commands" ''
224 tag login anonymous@${domain} anonymous
225 tag SELECT INBOX.comp.${orga}.repo1.0
226 tag FETCH 1 (BODY[HEADER])
227 tag LOGOUT
228 ''} | grep '^Message-ID: <repo1@root-1>'")
229
230 # TODO: Read a mail through public-inbox-nntpd
231 #machine.wait_for_unit("public-inbox-nntpd.socket")
232
233 # Delete a mail.
234 # Note that the use of an extension not listed in the addresses
235 # require to use --all
236 machine.succeed("curl -L https://machine.${domain}/inbox/repo1/repo1@root-1/raw | sudo -u public-inbox public-inbox-learn rm --all")
237 machine.fail("curl -L https://machine.${domain}/inbox/repo1/repo1@root-1/T/#u | grep 'This is a testing mail.'")
238
239 # Compact the database
240 machine.succeed("sudo -u public-inbox public-inbox-compact --all")
241 '';
242}