aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2019-10-18 19:43:39 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2019-10-18 19:43:39 +0200
commit8415083eb6acc343dfa404dbbc12fa0171a48a20 (patch)
treed83f54c99763ae49076bf3071449595b6ccae133
parent8fa7ff2c63fb0722144bc90837512d9f8b8c929d (diff)
downloadNix-8415083eb6acc343dfa404dbbc12fa0171a48a20.tar.gz
Nix-8415083eb6acc343dfa404dbbc12fa0171a48a20.tar.zst
Nix-8415083eb6acc343dfa404dbbc12fa0171a48a20.zip
Add new machine to nixops
-rw-r--r--modules/private/backup.nix6
-rw-r--r--modules/private/certificates.nix13
-rw-r--r--modules/private/databases/mariadb.nix2
-rw-r--r--modules/private/databases/openldap/default.nix2
-rw-r--r--modules/private/databases/postgresql.nix2
-rw-r--r--modules/private/databases/redis.nix2
-rw-r--r--modules/private/default.nix5
-rw-r--r--modules/private/dns.nix4
-rw-r--r--modules/private/ftp.nix2
-rw-r--r--modules/private/mail/default.nix42
-rw-r--r--modules/private/mail/dovecot.nix428
-rw-r--r--modules/private/mail/milters.nix208
-rw-r--r--modules/private/mail/postfix.nix488
-rw-r--r--modules/private/mail/rspamd.nix132
-rw-r--r--modules/private/mpd.nix3
-rw-r--r--modules/private/system/backup-2.nix24
-rw-r--r--modules/private/system/eldiron.nix6
-rw-r--r--modules/private/tasks/default.nix2
-rw-r--r--modules/private/websites/default.nix18
-rw-r--r--modules/private/websites/tools/mail/mta-sts.nix42
-rw-r--r--nixops/default.nix2
-rw-r--r--overlays/environments/immae-eu.nix2
-rw-r--r--overlays/nixops/default.nix1
-rw-r--r--overlays/nixops/hetzner_cloud.patch480
24 files changed, 1222 insertions, 694 deletions
diff --git a/modules/private/backup.nix b/modules/private/backup.nix
deleted file mode 100644
index 6911750..0000000
--- a/modules/private/backup.nix
+++ /dev/null
@@ -1,6 +0,0 @@
1{ ... }:
2{
3 config = {
4 services.backup.enable = true;
5 };
6}
diff --git a/modules/private/certificates.nix b/modules/private/certificates.nix
index cb284fc..9de3e6d 100644
--- a/modules/private/certificates.nix
+++ b/modules/private/certificates.nix
@@ -1,6 +1,7 @@
1{ lib, pkgs, config, ... }: 1{ lib, pkgs, config, ... }:
2{ 2{
3 options.services.myCertificates = { 3 options.myServices.certificates = {
4 enable = lib.mkEnableOption "enable certificates";
4 certConfig = lib.mkOption { 5 certConfig = lib.mkOption {
5 default = { 6 default = {
6 webroot = "${config.security.acme.directory}/acme-challenge"; 7 webroot = "${config.security.acme.directory}/acme-challenge";
@@ -14,18 +15,18 @@
14 }; 15 };
15 }; 16 };
16 17
17 config = { 18 config = lib.mkIf config.myServices.certificates.enable {
18 services.backup.profiles.system.excludeFile = '' 19 services.backup.profiles.system.excludeFile = ''
19 + ${config.security.acme.directory} 20 + ${config.security.acme.directory}
20 ''; 21 '';
21 services.websites.certs = config.services.myCertificates.certConfig; 22 services.websites.certs = config.myServices.certificates.certConfig;
22 myServices.databasesCerts = config.services.myCertificates.certConfig; 23 myServices.databasesCerts = config.myServices.certificates.certConfig;
23 myServices.ircCerts = config.services.myCertificates.certConfig; 24 myServices.ircCerts = config.myServices.certificates.certConfig;
24 25
25 security.acme.preliminarySelfsigned = true; 26 security.acme.preliminarySelfsigned = true;
26 27
27 security.acme.certs = { 28 security.acme.certs = {
28 "eldiron" = config.services.myCertificates.certConfig // { 29 "eldiron" = config.myServices.certificates.certConfig // {
29 domain = "eldiron.immae.eu"; 30 domain = "eldiron.immae.eu";
30 }; 31 };
31 }; 32 };
diff --git a/modules/private/databases/mariadb.nix b/modules/private/databases/mariadb.nix
index a7239c0..4293f02 100644
--- a/modules/private/databases/mariadb.nix
+++ b/modules/private/databases/mariadb.nix
@@ -5,7 +5,7 @@ in {
5 options.myServices.databases = { 5 options.myServices.databases = {
6 mariadb = { 6 mariadb = {
7 enable = lib.mkOption { 7 enable = lib.mkOption {
8 default = cfg.enable; 8 default = false;
9 example = true; 9 example = true;
10 description = "Whether to enable mariadb database"; 10 description = "Whether to enable mariadb database";
11 type = lib.types.bool; 11 type = lib.types.bool;
diff --git a/modules/private/databases/openldap/default.nix b/modules/private/databases/openldap/default.nix
index f09113a..9f72b29 100644
--- a/modules/private/databases/openldap/default.nix
+++ b/modules/private/databases/openldap/default.nix
@@ -48,7 +48,7 @@ in
48 options.myServices.databases = { 48 options.myServices.databases = {
49 openldap = { 49 openldap = {
50 enable = lib.mkOption { 50 enable = lib.mkOption {
51 default = cfg.enable; 51 default = false;
52 example = true; 52 example = true;
53 description = "Whether to enable ldap"; 53 description = "Whether to enable ldap";
54 type = lib.types.bool; 54 type = lib.types.bool;
diff --git a/modules/private/databases/postgresql.nix b/modules/private/databases/postgresql.nix
index 911a6d1..6d1901d 100644
--- a/modules/private/databases/postgresql.nix
+++ b/modules/private/databases/postgresql.nix
@@ -5,7 +5,7 @@ in {
5 options.myServices.databases = { 5 options.myServices.databases = {
6 postgresql = { 6 postgresql = {
7 enable = lib.mkOption { 7 enable = lib.mkOption {
8 default = cfg.enable; 8 default = false;
9 example = true; 9 example = true;
10 description = "Whether to enable postgresql database"; 10 description = "Whether to enable postgresql database";
11 type = lib.types.bool; 11 type = lib.types.bool;
diff --git a/modules/private/databases/redis.nix b/modules/private/databases/redis.nix
index 1ba6eed..c23ffec 100644
--- a/modules/private/databases/redis.nix
+++ b/modules/private/databases/redis.nix
@@ -4,7 +4,7 @@ let
4in { 4in {
5 options.myServices.databases.redis = { 5 options.myServices.databases.redis = {
6 enable = lib.mkOption { 6 enable = lib.mkOption {
7 default = cfg.enable; 7 default = false;
8 example = true; 8 example = true;
9 description = "Whether to enable redis database"; 9 description = "Whether to enable redis database";
10 type = lib.types.bool; 10 type = lib.types.bool;
diff --git a/modules/private/default.nix b/modules/private/default.nix
index 6dd7358..c418795 100644
--- a/modules/private/default.nix
+++ b/modules/private/default.nix
@@ -50,10 +50,6 @@ set = {
50 mailTool = ./websites/tools/mail; 50 mailTool = ./websites/tools/mail;
51 51
52 mail = ./mail; 52 mail = ./mail;
53 mailMilters = ./mail/milters.nix;
54 mailPostfix = ./mail/postfix.nix;
55 mailDovecot = ./mail/dovecot.nix;
56 mailRspamd = ./mail/rspamd.nix;
57 53
58 buildbot = ./buildbot; 54 buildbot = ./buildbot;
59 certificates = ./certificates.nix; 55 certificates = ./certificates.nix;
@@ -65,7 +61,6 @@ set = {
65 ftp = ./ftp.nix; 61 ftp = ./ftp.nix;
66 mpd = ./mpd.nix; 62 mpd = ./mpd.nix;
67 ssh = ./ssh; 63 ssh = ./ssh;
68 backup = ./backup.nix;
69 monitoring = ./monitoring; 64 monitoring = ./monitoring;
70 65
71 system = ./system.nix; 66 system = ./system.nix;
diff --git a/modules/private/dns.nix b/modules/private/dns.nix
index f0a3a5b..b4772fc 100644
--- a/modules/private/dns.nix
+++ b/modules/private/dns.nix
@@ -1,5 +1,6 @@
1{ lib, pkgs, config, myconfig, ... }: 1{ lib, pkgs, config, myconfig, ... }:
2{ 2{
3 options.myServices.dns.enable = lib.mkEnableOption "enable DNS resolver";
3 config = let 4 config = let
4 cfg = config.services.bind; 5 cfg = config.services.bind;
5 configFile = pkgs.writeText "named.conf" '' 6 configFile = pkgs.writeText "named.conf" ''
@@ -49,8 +50,7 @@
49 '') 50 '')
50 cfg.zones } 51 cfg.zones }
51 ''; 52 '';
52 in 53 in lib.mkIf config.myServices.dns.enable {
53 {
54 networking.firewall.allowedUDPPorts = [ 53 ]; 54 networking.firewall.allowedUDPPorts = [ 53 ];
55 networking.firewall.allowedTCPPorts = [ 53 ]; 55 networking.firewall.allowedTCPPorts = [ 53 ];
56 services.bind = { 56 services.bind = {
diff --git a/modules/private/ftp.nix b/modules/private/ftp.nix
index c6d7fbe..a1da32f 100644
--- a/modules/private/ftp.nix
+++ b/modules/private/ftp.nix
@@ -17,7 +17,7 @@ in
17 services.backup.profiles.ftp = { 17 services.backup.profiles.ftp = {
18 rootDir = "/var/lib/ftp"; 18 rootDir = "/var/lib/ftp";
19 }; 19 };
20 security.acme.certs."ftp" = config.services.myCertificates.certConfig // { 20 security.acme.certs."ftp" = config.myServices.certificates.certConfig // {
21 domain = "eldiron.immae.eu"; 21 domain = "eldiron.immae.eu";
22 postRun = '' 22 postRun = ''
23 systemctl restart pure-ftpd.service 23 systemctl restart pure-ftpd.service
diff --git a/modules/private/mail/default.nix b/modules/private/mail/default.nix
index ac8ad8c..d3b2a25 100644
--- a/modules/private/mail/default.nix
+++ b/modules/private/mail/default.nix
@@ -1,21 +1,31 @@
1{ lib, pkgs, config, myconfig, ... }: 1{ lib, pkgs, config, myconfig, ... }:
2{ 2{
3 config.security.acme.certs."mail" = config.services.myCertificates.certConfig // { 3 imports = [
4 domain = "eldiron.immae.eu"; 4 ./milters.nix
5 extraDomains = let 5 ./postfix.nix
6 zonesWithMx = builtins.filter (zone: 6 ./dovecot.nix
7 lib.attrsets.hasAttr "withEmail" zone && lib.lists.length zone.withEmail > 0 7 ./rspamd.nix
8 ) myconfig.env.dns.masterZones; 8 ];
9 mxs = map (zone: "mx-1.${zone.name}") zonesWithMx; 9 options.myServices.mail.enable = lib.mkEnableOption "enable Mail services";
10 in builtins.listToAttrs (map (mx: lib.attrsets.nameValuePair mx null) mxs); 10
11 }; 11 config = lib.mkIf config.myServices.mail.enable {
12 config.services.backup.profiles = { 12 security.acme.certs."mail" = config.myServices.certificates.certConfig // {
13 mail = { 13 domain = "eldiron.immae.eu";
14 rootDir = "/var/lib"; 14 extraDomains = let
15 excludeFile = lib.mkAfter '' 15 zonesWithMx = builtins.filter (zone:
16 + /var/lib/vhost 16 lib.attrsets.hasAttr "withEmail" zone && lib.lists.length zone.withEmail > 0
17 - /var/lib 17 ) myconfig.env.dns.masterZones;
18 ''; 18 mxs = map (zone: "mx-1.${zone.name}") zonesWithMx;
19 in builtins.listToAttrs (map (mx: lib.attrsets.nameValuePair mx null) mxs);
20 };
21 services.backup.profiles = {
22 mail = {
23 rootDir = "/var/lib";
24 excludeFile = lib.mkAfter ''
25 + /var/lib/vhost
26 - /var/lib
27 '';
28 };
19 }; 29 };
20 }; 30 };
21} 31}
diff --git a/modules/private/mail/dovecot.nix b/modules/private/mail/dovecot.nix
index 0d13a7b..dc75e0f 100644
--- a/modules/private/mail/dovecot.nix
+++ b/modules/private/mail/dovecot.nix
@@ -12,239 +12,241 @@ let
12 ''; 12 '';
13in 13in
14{ 14{
15 config.services.backup.profiles.mail.excludeFile = '' 15 config = lib.mkIf config.myServices.mail.enable {
16 + /var/lib/dhparams 16 services.backup.profiles.mail.excludeFile = ''
17 + /var/lib/dovecot 17 + /var/lib/dhparams
18 ''; 18 + /var/lib/dovecot
19 config.secrets.keys = [ 19 '';
20 { 20 secrets.keys = [
21 dest = "dovecot/ldap"; 21 {
22 user = config.services.dovecot2.user; 22 dest = "dovecot/ldap";
23 group = config.services.dovecot2.group; 23 user = config.services.dovecot2.user;
24 permissions = "0400"; 24 group = config.services.dovecot2.group;
25 text = '' 25 permissions = "0400";
26 hosts = ${myconfig.env.mail.dovecot.ldap.host} 26 text = ''
27 tls = yes 27 hosts = ${myconfig.env.mail.dovecot.ldap.host}
28 28 tls = yes
29 dn = ${myconfig.env.mail.dovecot.ldap.dn}
30 dnpass = ${myconfig.env.mail.dovecot.ldap.password}
31 29
32 auth_bind = yes 30 dn = ${myconfig.env.mail.dovecot.ldap.dn}
31 dnpass = ${myconfig.env.mail.dovecot.ldap.password}
33 32
34 ldap_version = 3 33 auth_bind = yes
35 34
36 base = ${myconfig.env.mail.dovecot.ldap.base} 35 ldap_version = 3
37 scope = subtree
38 36
39 user_filter = ${myconfig.env.mail.dovecot.ldap.filter} 37 base = ${myconfig.env.mail.dovecot.ldap.base}
40 pass_filter = ${myconfig.env.mail.dovecot.ldap.filter} 38 scope = subtree
41 39
42 user_attrs = ${myconfig.env.mail.dovecot.ldap.user_attrs} 40 user_filter = ${myconfig.env.mail.dovecot.ldap.filter}
43 pass_attrs = ${myconfig.env.mail.dovecot.ldap.pass_attrs} 41 pass_filter = ${myconfig.env.mail.dovecot.ldap.filter}
44 '';
45 }
46 ];
47 42
48 config.users.users.vhost = { 43 user_attrs = ${myconfig.env.mail.dovecot.ldap.user_attrs}
49 group = "vhost"; 44 pass_attrs = ${myconfig.env.mail.dovecot.ldap.pass_attrs}
50 uid = config.ids.uids.vhost; 45 '';
51 }; 46 }
52 config.users.groups.vhost.gid = config.ids.gids.vhost;
53
54 # https://blog.zeninc.net/index.php?post/2018/04/01/Un-annuaire-pour-les-gouverner-tous.......
55 config.services.dovecot2 = {
56 enable = true;
57 enablePAM = false;
58 enablePop3 = true;
59 enableImap = true;
60 enableLmtp = true;
61 protocols = [ "sieve" ];
62 modules = [
63 pkgs.dovecot_pigeonhole
64 pkgs.dovecot_fts-xapian
65 ];
66 mailUser = "vhost";
67 mailGroup = "vhost";
68 createMailUser = false;
69 mailboxes = [
70 { name = "Trash"; auto = "subscribe"; specialUse = "Trash"; }
71 { name = "Junk"; auto = "subscribe"; specialUse = "Junk"; }
72 { name = "Sent"; auto = "subscribe"; specialUse = "Sent"; }
73 { name = "Drafts"; auto = "subscribe"; specialUse = "Drafts"; }
74 ]; 47 ];
75 mailLocation = "mbox:~/Mail:INBOX=~/Mail/Inbox:INDEX=~/.imap"; 48
76 sslServerCert = "/var/lib/acme/mail/fullchain.pem"; 49 users.users.vhost = {
77 sslServerKey = "/var/lib/acme/mail/key.pem"; 50 group = "vhost";
78 sslCACert = "/var/lib/acme/mail/fullchain.pem"; 51 uid = config.ids.uids.vhost;
79 extraConfig = builtins.concatStringsSep "\n" [ 52 };
80 '' 53 users.groups.vhost.gid = config.ids.gids.vhost;
81 postmaster_address = postmaster@immae.eu 54
82 mail_attribute_dict = file:%h/dovecot-attributes 55 # https://blog.zeninc.net/index.php?post/2018/04/01/Un-annuaire-pour-les-gouverner-tous.......
83 imap_idle_notify_interval = 20 mins 56 services.dovecot2 = {
84 namespace inbox { 57 enable = true;
85 type = private 58 enablePAM = false;
86 separator = / 59 enablePop3 = true;
87 inbox = yes 60 enableImap = true;
88 list = yes 61 enableLmtp = true;
89 } 62 protocols = [ "sieve" ];
90 '' 63 modules = [
91 64 pkgs.dovecot_pigeonhole
92 # Full text search 65 pkgs.dovecot_fts-xapian
93 '' 66 ];
94 # needs to be bigger than any mailbox size 67 mailUser = "vhost";
95 default_vsz_limit = 2GB 68 mailGroup = "vhost";
96 mail_plugins = $mail_plugins fts fts_xapian 69 createMailUser = false;
70 mailboxes = [
71 { name = "Trash"; auto = "subscribe"; specialUse = "Trash"; }
72 { name = "Junk"; auto = "subscribe"; specialUse = "Junk"; }
73 { name = "Sent"; auto = "subscribe"; specialUse = "Sent"; }
74 { name = "Drafts"; auto = "subscribe"; specialUse = "Drafts"; }
75 ];
76 mailLocation = "mbox:~/Mail:INBOX=~/Mail/Inbox:INDEX=~/.imap";
77 sslServerCert = "/var/lib/acme/mail/fullchain.pem";
78 sslServerKey = "/var/lib/acme/mail/key.pem";
79 sslCACert = "/var/lib/acme/mail/fullchain.pem";
80 extraConfig = builtins.concatStringsSep "\n" [
81 ''
82 postmaster_address = postmaster@immae.eu
83 mail_attribute_dict = file:%h/dovecot-attributes
84 imap_idle_notify_interval = 20 mins
85 namespace inbox {
86 type = private
87 separator = /
88 inbox = yes
89 list = yes
90 }
91 ''
92
93 # Full text search
94 ''
95 # needs to be bigger than any mailbox size
96 default_vsz_limit = 2GB
97 mail_plugins = $mail_plugins fts fts_xapian
98 plugin {
99 plugin = fts fts_xapian
100 fts = xapian
101 fts_xapian = partial=2 full=20
102 fts_autoindex = yes
103 fts_autoindex_exclude = \Junk
104 fts_autoindex_exclude2 = \Trash
105 fts_autoindex_exclude3 = Virtual/*
106 }
107 ''
108
109 # Antispam
110 # https://docs.iredmail.org/dovecot.imapsieve.html
111 ''
112 # imap_sieve plugin added below
113
97 plugin { 114 plugin {
98 plugin = fts fts_xapian 115 sieve_plugins = sieve_imapsieve sieve_extprograms
99 fts = xapian 116 imapsieve_url = sieve://127.0.0.1:4190
100 fts_xapian = partial=2 full=20 117
101 fts_autoindex = yes 118 # From elsewhere to Junk folder
102 fts_autoindex_exclude = \Junk 119 imapsieve_mailbox1_name = Junk
103 fts_autoindex_exclude2 = \Trash 120 imapsieve_mailbox1_causes = COPY APPEND
104 fts_autoindex_exclude3 = Virtual/* 121 imapsieve_mailbox1_before = file:${./sieve_scripts}/report_spam.sieve;bindir=/var/lib/vhost/.imapsieve_bin
105 } 122
106 '' 123 # From Junk folder to elsewhere
107 124 imapsieve_mailbox2_name = *
108 # Antispam 125 imapsieve_mailbox2_from = Junk
109 # https://docs.iredmail.org/dovecot.imapsieve.html 126 imapsieve_mailbox2_causes = COPY
110 '' 127 imapsieve_mailbox2_before = file:${./sieve_scripts}/report_ham.sieve;bindir=/var/lib/vhost/.imapsieve_bin
111 # imap_sieve plugin added below 128
112 129 sieve_pipe_bin_dir = ${sieve_bin}
113 plugin { 130
114 sieve_plugins = sieve_imapsieve sieve_extprograms 131 sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
115 imapsieve_url = sieve://127.0.0.1:4190
116
117 # From elsewhere to Junk folder
118 imapsieve_mailbox1_name = Junk
119 imapsieve_mailbox1_causes = COPY APPEND
120 imapsieve_mailbox1_before = file:${./sieve_scripts}/report_spam.sieve;bindir=/var/lib/vhost/.imapsieve_bin
121
122 # From Junk folder to elsewhere
123 imapsieve_mailbox2_name = *
124 imapsieve_mailbox2_from = Junk
125 imapsieve_mailbox2_causes = COPY
126 imapsieve_mailbox2_before = file:${./sieve_scripts}/report_ham.sieve;bindir=/var/lib/vhost/.imapsieve_bin
127
128 sieve_pipe_bin_dir = ${sieve_bin}
129
130 sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
131 }
132 ''
133 # Services to listen
134 ''
135 service imap-login {
136 inet_listener imap {
137 } 132 }
138 inet_listener imaps { 133 ''
134 # Services to listen
135 ''
136 service imap-login {
137 inet_listener imap {
138 }
139 inet_listener imaps {
140 }
139 } 141 }
140 } 142 service pop3-login {
141 service pop3-login { 143 inet_listener pop3 {
142 inet_listener pop3 { 144 }
145 inet_listener pop3s {
146 }
143 } 147 }
144 inet_listener pop3s { 148 service imap {
145 } 149 }
146 } 150 service pop3 {
147 service imap {
148 }
149 service pop3 {
150 }
151 service auth {
152 unix_listener auth-userdb {
153 } 151 }
154 unix_listener ${config.services.postfix.config.queue_directory}/private/auth { 152 service auth {
155 mode = 0666 153 unix_listener auth-userdb {
154 }
155 unix_listener ${config.services.postfix.config.queue_directory}/private/auth {
156 mode = 0666
157 }
156 } 158 }
157 } 159 service auth-worker {
158 service auth-worker {
159 }
160 service dict {
161 unix_listener dict {
162 } 160 }
163 } 161 service dict {
164 service stats { 162 unix_listener dict {
165 unix_listener stats-reader { 163 }
166 user = vhost
167 group = vhost
168 mode = 0660
169 } 164 }
170 unix_listener stats-writer { 165 service stats {
171 user = vhost 166 unix_listener stats-reader {
172 group = vhost 167 user = vhost
173 mode = 0660 168 group = vhost
169 mode = 0660
170 }
171 unix_listener stats-writer {
172 user = vhost
173 group = vhost
174 mode = 0660
175 }
174 } 176 }
175 } 177 ''
176 '' 178
177 179 # Authentification
178 # Authentification 180 ''
179 '' 181 first_valid_uid = ${toString config.ids.uids.vhost}
180 first_valid_uid = ${toString config.ids.uids.vhost} 182 disable_plaintext_auth = yes
181 disable_plaintext_auth = yes 183 passdb {
182 passdb { 184 driver = ldap
183 driver = ldap 185 args = ${config.secrets.fullPaths."dovecot/ldap"}
184 args = ${config.secrets.fullPaths."dovecot/ldap"} 186 }
185 } 187 userdb {
186 userdb { 188 driver = static
187 driver = static 189 args = user=%u uid=vhost gid=vhost home=/var/lib/vhost/%d/%n/ mail=mbox:~/Mail:INBOX=~/Mail/Inbox:INDEX=~/.imap
188 args = user=%u uid=vhost gid=vhost home=/var/lib/vhost/%d/%n/ mail=mbox:~/Mail:INBOX=~/Mail/Inbox:INDEX=~/.imap 190 }
189 } 191 ''
190 ''
191
192 # Zlib
193 ''
194 mail_plugins = $mail_plugins zlib
195 plugin {
196 zlib_save_level = 6
197 zlib_save = gz
198 }
199 ''
200 192
201 # Sieve 193 # Zlib
202 '' 194 ''
203 plugin { 195 mail_plugins = $mail_plugins zlib
204 sieve = file:~/sieve;bindir=~/.sieve-bin;active=~/.dovecot.sieve 196 plugin {
205 } 197 zlib_save_level = 6
206 service managesieve-login { 198 zlib_save = gz
207 } 199 }
208 service managesieve { 200 ''
209 }
210 ''
211
212 # Virtual mailboxes
213 ''
214 mail_plugins = $mail_plugins virtual
215 namespace Virtual {
216 prefix = Virtual/
217 location = virtual:~/Virtual
218 }
219 ''
220 201
221 # Protocol specific configuration 202 # Sieve
222 # Needs to come last if there are mail_plugins entries 203 ''
223 '' 204 plugin {
224 protocol imap { 205 sieve = file:~/sieve;bindir=~/.sieve-bin;active=~/.dovecot.sieve
225 mail_plugins = $mail_plugins imap_sieve 206 }
226 } 207 service managesieve-login {
227 protocol lda { 208 }
228 mail_plugins = $mail_plugins sieve 209 service managesieve {
229 } 210 }
230 '' 211 ''
231 ]; 212
232 }; 213 # Virtual mailboxes
233 config.networking.firewall.allowedTCPPorts = [ 110 143 993 995 4190 ]; 214 ''
234 config.system.activationScripts.dovecot = { 215 mail_plugins = $mail_plugins virtual
235 deps = [ "users" ]; 216 namespace Virtual {
236 text ='' 217 prefix = Virtual/
237 install -m 0755 -o vhost -g vhost -d /var/lib/vhost 218 location = virtual:~/Virtual
238 ''; 219 }
239 }; 220 ''
240 221
241 config.security.acme.certs."mail" = { 222 # Protocol specific configuration
242 postRun = '' 223 # Needs to come last if there are mail_plugins entries
243 systemctl restart dovecot2.service 224 ''
244 ''; 225 protocol imap {
245 extraDomains = { 226 mail_plugins = $mail_plugins imap_sieve
246 "imap.immae.eu" = null; 227 }
247 "pop3.immae.eu" = null; 228 protocol lda {
229 mail_plugins = $mail_plugins sieve
230 }
231 ''
232 ];
233 };
234 networking.firewall.allowedTCPPorts = [ 110 143 993 995 4190 ];
235 system.activationScripts.dovecot = {
236 deps = [ "users" ];
237 text =''
238 install -m 0755 -o vhost -g vhost -d /var/lib/vhost
239 '';
240 };
241
242 security.acme.certs."mail" = {
243 postRun = ''
244 systemctl restart dovecot2.service
245 '';
246 extraDomains = {
247 "imap.immae.eu" = null;
248 "pop3.immae.eu" = null;
249 };
248 }; 250 };
249 }; 251 };
250} 252}
diff --git a/modules/private/mail/milters.nix b/modules/private/mail/milters.nix
index c4bd990..123af4a 100644
--- a/modules/private/mail/milters.nix
+++ b/modules/private/mail/milters.nix
@@ -12,112 +12,114 @@
12 milters sockets 12 milters sockets
13 ''; 13 '';
14 }; 14 };
15 config.secrets.keys = [ 15 config = lib.mkIf config.myServices.mail.enable {
16 { 16 secrets.keys = [
17 dest = "opendkim/eldiron.private"; 17 {
18 user = config.services.opendkim.user; 18 dest = "opendkim/eldiron.private";
19 group = config.services.opendkim.group; 19 user = config.services.opendkim.user;
20 permissions = "0400"; 20 group = config.services.opendkim.group;
21 text = myconfig.env.mail.dkim.eldiron.private; 21 permissions = "0400";
22 } 22 text = myconfig.env.mail.dkim.eldiron.private;
23 { 23 }
24 dest = "opendkim/eldiron.txt"; 24 {
25 user = config.services.opendkim.user; 25 dest = "opendkim/eldiron.txt";
26 group = config.services.opendkim.group; 26 user = config.services.opendkim.user;
27 permissions = "0444"; 27 group = config.services.opendkim.group;
28 text = '' 28 permissions = "0444";
29 eldiron._domainkey IN TXT ${myconfig.env.mail.dkim.eldiron.public}''; 29 text = ''
30 } 30 eldiron._domainkey IN TXT ${myconfig.env.mail.dkim.eldiron.public}'';
31 { 31 }
32 dest = "opendmarc/ignore.hosts"; 32 {
33 user = config.services.opendmarc.user; 33 dest = "opendmarc/ignore.hosts";
34 group = config.services.opendmarc.group; 34 user = config.services.opendmarc.user;
35 permissions = "0400"; 35 group = config.services.opendmarc.group;
36 text = myconfig.env.mail.dmarc.ignore_hosts; 36 permissions = "0400";
37 } 37 text = myconfig.env.mail.dmarc.ignore_hosts;
38 ]; 38 }
39 config.users.users."${config.services.opendkim.user}".extraGroups = [ "keys" ];
40 config.services.opendkim = {
41 enable = true;
42 socket = "local:${config.myServices.mail.milters.sockets.opendkim}";
43 domains = builtins.concatStringsSep "," (lib.flatten (map
44 (zone: map
45 (e: "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}")
46 (zone.withEmail or [])
47 )
48 myconfig.env.dns.masterZones
49 ));
50 keyPath = "${config.secrets.location}/opendkim";
51 selector = "eldiron";
52 configFile = pkgs.writeText "opendkim.conf" ''
53 SubDomains yes
54 UMask 002
55 '';
56 group = config.services.postfix.group;
57 };
58 config.systemd.services.opendkim.preStart = lib.mkBefore ''
59 # Skip the prestart script as keys are handled in secrets
60 exit 0
61 '';
62 config.services.filesWatcher.opendkim = {
63 restart = true;
64 paths = [
65 config.secrets.fullPaths."opendkim/eldiron.private"
66 ]; 39 ];
67 }; 40 users.users."${config.services.opendkim.user}".extraGroups = [ "keys" ];
68 41 services.opendkim = {
69 config.users.users."${config.services.opendmarc.user}".extraGroups = [ "keys" ]; 42 enable = true;
70 config.services.opendmarc = { 43 socket = "local:${config.myServices.mail.milters.sockets.opendkim}";
71 enable = true; 44 domains = builtins.concatStringsSep "," (lib.flatten (map
72 socket = "local:${config.myServices.mail.milters.sockets.opendmarc}"; 45 (zone: map
73 configFile = pkgs.writeText "opendmarc.conf" '' 46 (e: "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}")
74 AuthservID HOSTNAME 47 (zone.withEmail or [])
75 FailureReports false 48 )
76 FailureReportsBcc postmaster@localhost.immae.eu 49 myconfig.env.dns.masterZones
77 FailureReportsOnNone true 50 ));
78 FailureReportsSentBy postmaster@immae.eu 51 keyPath = "${config.secrets.location}/opendkim";
79 IgnoreAuthenticatedClients true 52 selector = "eldiron";
80 IgnoreHosts ${config.secrets.fullPaths."opendmarc/ignore.hosts"} 53 configFile = pkgs.writeText "opendkim.conf" ''
81 SoftwareHeader true 54 SubDomains yes
82 SPFSelfValidate true 55 UMask 002
83 TrustedAuthservIDs HOSTNAME, immae.eu, nef2.ens.fr 56 '';
84 UMask 002 57 group = config.services.postfix.group;
58 };
59 systemd.services.opendkim.preStart = lib.mkBefore ''
60 # Skip the prestart script as keys are handled in secrets
61 exit 0
85 ''; 62 '';
86 group = config.services.postfix.group; 63 services.filesWatcher.opendkim = {
87 }; 64 restart = true;
88 config.services.filesWatcher.opendmarc = { 65 paths = [
89 restart = true; 66 config.secrets.fullPaths."opendkim/eldiron.private"
90 paths = [ 67 ];
91 config.secrets.fullPaths."opendmarc/ignore.hosts" 68 };
92 ]; 69
93 }; 70 users.users."${config.services.opendmarc.user}".extraGroups = [ "keys" ];
71 services.opendmarc = {
72 enable = true;
73 socket = "local:${config.myServices.mail.milters.sockets.opendmarc}";
74 configFile = pkgs.writeText "opendmarc.conf" ''
75 AuthservID HOSTNAME
76 FailureReports false
77 FailureReportsBcc postmaster@localhost.immae.eu
78 FailureReportsOnNone true
79 FailureReportsSentBy postmaster@immae.eu
80 IgnoreAuthenticatedClients true
81 IgnoreHosts ${config.secrets.fullPaths."opendmarc/ignore.hosts"}
82 SoftwareHeader true
83 SPFSelfValidate true
84 TrustedAuthservIDs HOSTNAME, immae.eu, nef2.ens.fr
85 UMask 002
86 '';
87 group = config.services.postfix.group;
88 };
89 services.filesWatcher.opendmarc = {
90 restart = true;
91 paths = [
92 config.secrets.fullPaths."opendmarc/ignore.hosts"
93 ];
94 };
94 95
95 config.services.openarc = { 96 services.openarc = {
96 enable = true; 97 enable = true;
97 user = "opendkim"; 98 user = "opendkim";
98 socket = "local:${config.myServices.mail.milters.sockets.openarc}"; 99 socket = "local:${config.myServices.mail.milters.sockets.openarc}";
99 group = config.services.postfix.group; 100 group = config.services.postfix.group;
100 configFile = pkgs.writeText "openarc.conf" '' 101 configFile = pkgs.writeText "openarc.conf" ''
101 AuthservID mail.immae.eu 102 AuthservID mail.immae.eu
102 Domain mail.immae.eu 103 Domain mail.immae.eu
103 KeyFile ${config.secrets.fullPaths."opendkim/eldiron.private"} 104 KeyFile ${config.secrets.fullPaths."opendkim/eldiron.private"}
104 Mode sv 105 Mode sv
105 Selector eldiron 106 Selector eldiron
106 SoftwareHeader yes 107 SoftwareHeader yes
107 Syslog Yes 108 Syslog Yes
109 '';
110 };
111 systemd.services.openarc.postStart = lib.optionalString
112 (lib.strings.hasPrefix "local:" config.services.openarc.socket) ''
113 while [ ! -S ${lib.strings.removePrefix "local:" config.services.openarc.socket} ]; do
114 sleep 0.5
115 done
116 chmod g+w ${lib.strings.removePrefix "local:" config.services.openarc.socket}
108 ''; 117 '';
109 }; 118 services.filesWatcher.openarc = {
110 config.systemd.services.openarc.postStart = lib.optionalString 119 restart = true;
111 (lib.strings.hasPrefix "local:" config.services.openarc.socket) '' 120 paths = [
112 while [ ! -S ${lib.strings.removePrefix "local:" config.services.openarc.socket} ]; do 121 config.secrets.fullPaths."opendkim/eldiron.private"
113 sleep 0.5 122 ];
114 done 123 };
115 chmod g+w ${lib.strings.removePrefix "local:" config.services.openarc.socket}
116 '';
117 config.services.filesWatcher.openarc = {
118 restart = true;
119 paths = [
120 config.secrets.fullPaths."opendkim/eldiron.private"
121 ];
122 }; 124 };
123} 125}
diff --git a/modules/private/mail/postfix.nix b/modules/private/mail/postfix.nix
index edfd196..9fdc7bd 100644
--- a/modules/private/mail/postfix.nix
+++ b/modules/private/mail/postfix.nix
@@ -1,267 +1,269 @@
1{ lib, pkgs, config, myconfig, ... }: 1{ lib, pkgs, config, myconfig, ... }:
2{ 2{
3 config.services.backup.profiles.mail.excludeFile = '' 3 config = lib.mkIf config.myServices.mail.enable {
4 + /var/lib/postfix 4 services.backup.profiles.mail.excludeFile = ''
5 ''; 5 + /var/lib/postfix
6 config.secrets.keys = [ 6 '';
7 { 7 secrets.keys = [
8 dest = "postfix/mysql_alias_maps"; 8 {
9 user = config.services.postfix.user; 9 dest = "postfix/mysql_alias_maps";
10 group = config.services.postfix.group; 10 user = config.services.postfix.user;
11 permissions = "0440"; 11 group = config.services.postfix.group;
12 text = '' 12 permissions = "0440";
13 # We need to specify that option to trigger ssl connection 13 text = ''
14 tls_ciphers = TLSv1.2 14 # We need to specify that option to trigger ssl connection
15 user = ${myconfig.env.mail.postfix.mysql.user} 15 tls_ciphers = TLSv1.2
16 password = ${myconfig.env.mail.postfix.mysql.password} 16 user = ${myconfig.env.mail.postfix.mysql.user}
17 hosts = unix:${myconfig.env.mail.postfix.mysql.socket} 17 password = ${myconfig.env.mail.postfix.mysql.password}
18 dbname = ${myconfig.env.mail.postfix.mysql.database} 18 hosts = unix:${myconfig.env.mail.postfix.mysql.socket}
19 query = SELECT DISTINCT destination 19 dbname = ${myconfig.env.mail.postfix.mysql.database}
20 FROM forwardings_merge 20 query = SELECT DISTINCT destination
21 WHERE 21 FROM forwardings_merge
22 ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s')) 22 WHERE
23 AND active = 1 23 ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s'))
24 AND '%s' NOT IN 24 AND active = 1
25 ( 25 AND '%s' NOT IN
26 SELECT source 26 (
27 SELECT source
28 FROM forwardings_blacklisted
29 WHERE source = '%s'
30 ) UNION
31 SELECT 'devnull@immae.eu'
27 FROM forwardings_blacklisted 32 FROM forwardings_blacklisted
28 WHERE source = '%s' 33 WHERE source = '%s'
29 ) UNION 34 '';
30 SELECT 'devnull@immae.eu' 35 }
31 FROM forwardings_blacklisted 36 {
32 WHERE source = '%s' 37 dest = "postfix/mysql_mailbox_maps";
33 ''; 38 user = config.services.postfix.user;
34 } 39 group = config.services.postfix.group;
35 { 40 permissions = "0440";
36 dest = "postfix/mysql_mailbox_maps"; 41 text = ''
37 user = config.services.postfix.user; 42 # We need to specify that option to trigger ssl connection
38 group = config.services.postfix.group; 43 tls_ciphers = TLSv1.2
39 permissions = "0440"; 44 user = ${myconfig.env.mail.postfix.mysql.user}
40 text = '' 45 password = ${myconfig.env.mail.postfix.mysql.password}
41 # We need to specify that option to trigger ssl connection 46 hosts = unix:${myconfig.env.mail.postfix.mysql.socket}
42 tls_ciphers = TLSv1.2 47 dbname = ${myconfig.env.mail.postfix.mysql.database}
43 user = ${myconfig.env.mail.postfix.mysql.user} 48 result_format = /%d/%u
44 password = ${myconfig.env.mail.postfix.mysql.password} 49 query = SELECT DISTINCT '%s'
45 hosts = unix:${myconfig.env.mail.postfix.mysql.socket} 50 FROM mailboxes
46 dbname = ${myconfig.env.mail.postfix.mysql.database} 51 WHERE active = 1
47 result_format = /%d/%u 52 AND (
48 query = SELECT DISTINCT '%s' 53 (domain = '%d' AND user = '%u' AND regex = 0)
49 FROM mailboxes 54 OR (
50 WHERE active = 1 55 regex = 1
51 AND ( 56 AND '%d' REGEXP CONCAT('^',domain,'$')
52 (domain = '%d' AND user = '%u' AND regex = 0) 57 AND '%u' REGEXP CONCAT('^',user,'$')
53 OR ( 58 )
54 regex = 1
55 AND '%d' REGEXP CONCAT('^',domain,'$')
56 AND '%u' REGEXP CONCAT('^',user,'$')
57 ) 59 )
58 ) 60 LIMIT 1
59 LIMIT 1
60 '';
61 }
62 {
63 dest = "postfix/mysql_sender_login_maps";
64 user = config.services.postfix.user;
65 group = config.services.postfix.group;
66 permissions = "0440";
67 text = ''
68 # We need to specify that option to trigger ssl connection
69 tls_ciphers = TLSv1.2
70 user = ${myconfig.env.mail.postfix.mysql.user}
71 password = ${myconfig.env.mail.postfix.mysql.password}
72 hosts = unix:${myconfig.env.mail.postfix.mysql.socket}
73 dbname = ${myconfig.env.mail.postfix.mysql.database}
74 query = SELECT DISTINCT destination
75 FROM forwardings_merge
76 WHERE
77 ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s'))
78 AND active = 1
79 UNION SELECT '%s' AS destination
80 ''; 61 '';
81 } 62 }
82 ]; 63 {
64 dest = "postfix/mysql_sender_login_maps";
65 user = config.services.postfix.user;
66 group = config.services.postfix.group;
67 permissions = "0440";
68 text = ''
69 # We need to specify that option to trigger ssl connection
70 tls_ciphers = TLSv1.2
71 user = ${myconfig.env.mail.postfix.mysql.user}
72 password = ${myconfig.env.mail.postfix.mysql.password}
73 hosts = unix:${myconfig.env.mail.postfix.mysql.socket}
74 dbname = ${myconfig.env.mail.postfix.mysql.database}
75 query = SELECT DISTINCT destination
76 FROM forwardings_merge
77 WHERE
78 ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s'))
79 AND active = 1
80 UNION SELECT '%s' AS destination
81 '';
82 }
83 ];
83 84
84 config.networking.firewall.allowedTCPPorts = [ 25 465 587 ]; 85 networking.firewall.allowedTCPPorts = [ 25 465 587 ];
85 86
86 config.nixpkgs.overlays = [ (self: super: { 87 nixpkgs.overlays = [ (self: super: {
87 postfix = super.postfix.override { withMySQL = true; }; 88 postfix = super.postfix.override { withMySQL = true; };
88 }) ]; 89 }) ];
89 config.users.users."${config.services.postfix.user}".extraGroups = [ "keys" ]; 90 users.users."${config.services.postfix.user}".extraGroups = [ "keys" ];
90 config.services.filesWatcher.postfix = { 91 services.filesWatcher.postfix = {
91 restart = true; 92 restart = true;
92 paths = [ 93 paths = [
93 config.secrets.fullPaths."postfix/mysql_alias_maps" 94 config.secrets.fullPaths."postfix/mysql_alias_maps"
94 config.secrets.fullPaths."postfix/mysql_mailbox_maps" 95 config.secrets.fullPaths."postfix/mysql_mailbox_maps"
95 config.secrets.fullPaths."postfix/mysql_sender_login_maps" 96 config.secrets.fullPaths."postfix/mysql_sender_login_maps"
96 ]; 97 ];
97 }; 98 };
98 config.services.postfix = { 99 services.postfix = {
99 mapFiles = let 100 mapFiles = let
100 recipient_maps = let 101 recipient_maps = let
101 name = n: i: "relay_${n}_${toString i}"; 102 name = n: i: "relay_${n}_${toString i}";
102 pair = n: i: m: lib.attrsets.nameValuePair (name n i) ( 103 pair = n: i: m: lib.attrsets.nameValuePair (name n i) (
103 if m.type == "hash" 104 if m.type == "hash"
104 then pkgs.writeText (name n i) m.content 105 then pkgs.writeText (name n i) m.content
105 else null
106 );
107 pairs = n: v: lib.imap1 (i: m: pair n i m) v.recipient_maps;
108 in lib.attrsets.filterAttrs (k: v: v != null) (
109 lib.attrsets.listToAttrs (lib.flatten (
110 lib.attrsets.mapAttrsToList pairs myconfig.env.mail.postfix.backup_domains
111 ))
112 );
113 relay_restrictions = lib.attrsets.filterAttrs (k: v: v != null) (
114 lib.attrsets.mapAttrs' (n: v:
115 lib.attrsets.nameValuePair "recipient_access_${n}" (
116 if lib.attrsets.hasAttr "relay_restrictions" v
117 then pkgs.writeText "recipient_access_${n}" v.relay_restrictions
118 else null 106 else null
119 ) 107 );
120 ) myconfig.env.mail.postfix.backup_domains 108 pairs = n: v: lib.imap1 (i: m: pair n i m) v.recipient_maps;
121 ); 109 in lib.attrsets.filterAttrs (k: v: v != null) (
122 in 110 lib.attrsets.listToAttrs (lib.flatten (
123 recipient_maps // relay_restrictions; 111 lib.attrsets.mapAttrsToList pairs myconfig.env.mail.postfix.backup_domains
124 config = { 112 ))
125 ### postfix module overrides 113 );
126 readme_directory = "${pkgs.postfix}/share/postfix/doc"; 114 relay_restrictions = lib.attrsets.filterAttrs (k: v: v != null) (
127 smtp_tls_CAfile = lib.mkForce ""; 115 lib.attrsets.mapAttrs' (n: v:
128 smtp_tls_cert_file = lib.mkForce ""; 116 lib.attrsets.nameValuePair "recipient_access_${n}" (
129 smtp_tls_key_file = lib.mkForce ""; 117 if lib.attrsets.hasAttr "relay_restrictions" v
118 then pkgs.writeText "recipient_access_${n}" v.relay_restrictions
119 else null
120 )
121 ) myconfig.env.mail.postfix.backup_domains
122 );
123 in
124 recipient_maps // relay_restrictions;
125 config = {
126 ### postfix module overrides
127 readme_directory = "${pkgs.postfix}/share/postfix/doc";
128 smtp_tls_CAfile = lib.mkForce "";
129 smtp_tls_cert_file = lib.mkForce "";
130 smtp_tls_key_file = lib.mkForce "";
130 131
131 message_size_limit = "1073741824"; # Don't put 0 here, it's not equivalent to "unlimited" 132 message_size_limit = "1073741824"; # Don't put 0 here, it's not equivalent to "unlimited"
132 alias_database = "\$alias_maps"; 133 alias_database = "\$alias_maps";
133 134
134 ### Virtual mailboxes config 135 ### Virtual mailboxes config
135 virtual_alias_maps = "mysql:${config.secrets.fullPaths."postfix/mysql_alias_maps"}"; 136 virtual_alias_maps = "mysql:${config.secrets.fullPaths."postfix/mysql_alias_maps"}";
136 virtual_mailbox_domains = myconfig.env.mail.postfix.additional_mailbox_domains 137 virtual_mailbox_domains = myconfig.env.mail.postfix.additional_mailbox_domains
137 ++ lib.remove "localhost.immae.eu" (lib.remove null (lib.flatten (map 138 ++ lib.remove "localhost.immae.eu" (lib.remove null (lib.flatten (map
138 (zone: map 139 (zone: map
139 (e: if e.receive 140 (e: if e.receive
140 then "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}" 141 then "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}"
141 else null 142 else null
143 )
144 (zone.withEmail or [])
142 ) 145 )
143 (zone.withEmail or []) 146 myconfig.env.dns.masterZones
144 ) 147 )));
145 myconfig.env.dns.masterZones 148 virtual_mailbox_maps = "mysql:${config.secrets.fullPaths."postfix/mysql_mailbox_maps"}";
146 ))); 149 dovecot_destination_recipient_limit = "1";
147 virtual_mailbox_maps = "mysql:${config.secrets.fullPaths."postfix/mysql_mailbox_maps"}"; 150 virtual_transport = "dovecot";
148 dovecot_destination_recipient_limit = "1";
149 virtual_transport = "dovecot";
150 151
151 ### Relay domains 152 ### Relay domains
152 relay_domains = lib.flatten (lib.attrsets.mapAttrsToList (n: v: v.domains or []) myconfig.env.mail.postfix.backup_domains); 153 relay_domains = lib.flatten (lib.attrsets.mapAttrsToList (n: v: v.domains or []) myconfig.env.mail.postfix.backup_domains);
153 relay_recipient_maps = lib.flatten (lib.attrsets.mapAttrsToList (n: v: 154 relay_recipient_maps = lib.flatten (lib.attrsets.mapAttrsToList (n: v:
154 lib.imap1 (i: m: "${m.type}:/etc/postfix/relay_${n}_${toString i}") v.recipient_maps 155 lib.imap1 (i: m: "${m.type}:/etc/postfix/relay_${n}_${toString i}") v.recipient_maps
155 ) myconfig.env.mail.postfix.backup_domains); 156 ) myconfig.env.mail.postfix.backup_domains);
156 smtpd_relay_restrictions = [ 157 smtpd_relay_restrictions = [
157 "permit_mynetworks" 158 "permit_mynetworks"
158 "permit_sasl_authenticated" 159 "permit_sasl_authenticated"
159 "defer_unauth_destination" 160 "defer_unauth_destination"
160 ] ++ lib.flatten (lib.attrsets.mapAttrsToList (n: v: 161 ] ++ lib.flatten (lib.attrsets.mapAttrsToList (n: v:
161 if lib.attrsets.hasAttr "relay_restrictions" v 162 if lib.attrsets.hasAttr "relay_restrictions" v
162 then [ "check_recipient_access hash:/etc/postfix/recipient_access_${n}" ] 163 then [ "check_recipient_access hash:/etc/postfix/recipient_access_${n}" ]
163 else [] 164 else []
164 ) myconfig.env.mail.postfix.backup_domains); 165 ) myconfig.env.mail.postfix.backup_domains);
165 166
166 ### Additional smtpd configuration 167 ### Additional smtpd configuration
167 smtpd_tls_received_header = "yes"; 168 smtpd_tls_received_header = "yes";
168 smtpd_tls_loglevel = "1"; 169 smtpd_tls_loglevel = "1";
169 170
170 ### Email sending configuration 171 ### Email sending configuration
171 smtp_tls_security_level = "may"; 172 smtp_tls_security_level = "may";
172 smtp_tls_loglevel = "1"; 173 smtp_tls_loglevel = "1";
173 174
174 ### Force ip bind for smtp 175 ### Force ip bind for smtp
175 smtp_bind_address = myconfig.env.servers.eldiron.ips.main.ip4; 176 smtp_bind_address = myconfig.env.servers.eldiron.ips.main.ip4;
176 smtp_bind_address6 = builtins.head myconfig.env.servers.eldiron.ips.main.ip6; 177 smtp_bind_address6 = builtins.head myconfig.env.servers.eldiron.ips.main.ip6;
177 178
178 # #Unneeded if postfix can only send e-mail from "self" domains 179 # #Unneeded if postfix can only send e-mail from "self" domains
179 # #smtp_sasl_auth_enable = "yes"; 180 # #smtp_sasl_auth_enable = "yes";
180 # #smtp_sasl_password_maps = "hash:/etc/postfix/relay_creds"; 181 # #smtp_sasl_password_maps = "hash:/etc/postfix/relay_creds";
181 # #smtp_sasl_security_options = "noanonymous"; 182 # #smtp_sasl_security_options = "noanonymous";
182 # #smtp_sender_dependent_authentication = "yes"; 183 # #smtp_sender_dependent_authentication = "yes";
183 # #sender_dependent_relayhost_maps = "hash:/etc/postfix/sender_relay"; 184 # #sender_dependent_relayhost_maps = "hash:/etc/postfix/sender_relay";
184 185
185 ### opendkim, opendmarc, openarc milters 186 ### opendkim, opendmarc, openarc milters
186 non_smtpd_milters = [ 187 non_smtpd_milters = [
187 "unix:${config.myServices.mail.milters.sockets.opendkim}" 188 "unix:${config.myServices.mail.milters.sockets.opendkim}"
188 "unix:${config.myServices.mail.milters.sockets.opendmarc}" 189 "unix:${config.myServices.mail.milters.sockets.opendmarc}"
189 "unix:${config.myServices.mail.milters.sockets.openarc}" 190 "unix:${config.myServices.mail.milters.sockets.openarc}"
190 ]; 191 ];
191 smtpd_milters = [ 192 smtpd_milters = [
192 "unix:${config.myServices.mail.milters.sockets.opendkim}" 193 "unix:${config.myServices.mail.milters.sockets.opendkim}"
193 "unix:${config.myServices.mail.milters.sockets.opendmarc}" 194 "unix:${config.myServices.mail.milters.sockets.opendmarc}"
194 "unix:${config.myServices.mail.milters.sockets.openarc}" 195 "unix:${config.myServices.mail.milters.sockets.openarc}"
195 ];
196 };
197 enable = true;
198 enableSmtp = true;
199 enableSubmission = true;
200 submissionOptions = {
201 smtpd_tls_security_level = "encrypt";
202 smtpd_sasl_auth_enable = "yes";
203 smtpd_tls_auth_only = "yes";
204 smtpd_sasl_tls_security_options = "noanonymous";
205 smtpd_sasl_type = "dovecot";
206 smtpd_sasl_path = "private/auth";
207 smtpd_reject_unlisted_recipient = "no";
208 smtpd_client_restrictions = "permit_sasl_authenticated,reject";
209 # Refuse to send e-mails with a From that is not handled
210 smtpd_sender_restrictions =
211 "reject_sender_login_mismatch,reject_unlisted_sender,permit_sasl_authenticated,reject";
212 smtpd_sender_login_maps = "mysql:${config.secrets.fullPaths."postfix/mysql_sender_login_maps"}";
213 smtpd_recipient_restrictions = "permit_sasl_authenticated,reject";
214 milter_macro_daemon_name = "ORIGINATING";
215 smtpd_milters = "unix:${config.myServices.mail.milters.sockets.opendkim}";
216 };
217 # FIXME: Mail adressed to localhost.immae.eu will still have mx-1 as
218 # prioritized MX, which provokes "mail for localhost.immae.eu loops
219 # back to myself" errors. This transport entry forces to push
220 # e-mails to its right destination.
221 transport = ''
222 localhost.immae.eu smtp:[immae.eu]:25
223 '';
224 destination = ["localhost"];
225 # This needs to reverse DNS
226 hostname = "eldiron.immae.eu";
227 setSendmail = true;
228 sslCert = "/var/lib/acme/mail/fullchain.pem";
229 sslKey = "/var/lib/acme/mail/key.pem";
230 recipientDelimiter = "+";
231 masterConfig = {
232 submissions = {
233 type = "inet";
234 private = false;
235 command = "smtpd";
236 args = ["-o" "smtpd_tls_wrappermode=yes" ] ++ (let
237 mkKeyVal = opt: val: [ "-o" (opt + "=" + val) ];
238 in lib.concatLists (lib.mapAttrsToList mkKeyVal config.services.postfix.submissionOptions)
239 );
240 };
241 dovecot = {
242 type = "unix";
243 privileged = true;
244 chroot = false;
245 command = "pipe";
246 args = let
247 # rspamd could be used as a milter, but then it cannot apply
248 # its checks "per user" (milter is not yet dispatched to
249 # users), so we wrap dovecot-lda inside rspamc per recipient
250 # here.
251 dovecot_exe = "${pkgs.dovecot}/libexec/dovecot/dovecot-lda -f \${sender} -a \${original_recipient} -d \${user}@\${nexthop}";
252 in [
253 "flags=DRhu" "user=vhost:vhost"
254 "argv=${pkgs.rspamd}/bin/rspamc -h ${config.myServices.mail.rspamd.sockets.worker-controller} -c bayes -d \${user}@\${nexthop} --mime --exec {${dovecot_exe}}"
255 ]; 196 ];
256 }; 197 };
198 enable = true;
199 enableSmtp = true;
200 enableSubmission = true;
201 submissionOptions = {
202 smtpd_tls_security_level = "encrypt";
203 smtpd_sasl_auth_enable = "yes";
204 smtpd_tls_auth_only = "yes";
205 smtpd_sasl_tls_security_options = "noanonymous";
206 smtpd_sasl_type = "dovecot";
207 smtpd_sasl_path = "private/auth";
208 smtpd_reject_unlisted_recipient = "no";
209 smtpd_client_restrictions = "permit_sasl_authenticated,reject";
210 # Refuse to send e-mails with a From that is not handled
211 smtpd_sender_restrictions =
212 "reject_sender_login_mismatch,reject_unlisted_sender,permit_sasl_authenticated,reject";
213 smtpd_sender_login_maps = "mysql:${config.secrets.fullPaths."postfix/mysql_sender_login_maps"}";
214 smtpd_recipient_restrictions = "permit_sasl_authenticated,reject";
215 milter_macro_daemon_name = "ORIGINATING";
216 smtpd_milters = "unix:${config.myServices.mail.milters.sockets.opendkim}";
217 };
218 # FIXME: Mail adressed to localhost.immae.eu will still have mx-1 as
219 # prioritized MX, which provokes "mail for localhost.immae.eu loops
220 # back to myself" errors. This transport entry forces to push
221 # e-mails to its right destination.
222 transport = ''
223 localhost.immae.eu smtp:[immae.eu]:25
224 '';
225 destination = ["localhost"];
226 # This needs to reverse DNS
227 hostname = "eldiron.immae.eu";
228 setSendmail = true;
229 sslCert = "/var/lib/acme/mail/fullchain.pem";
230 sslKey = "/var/lib/acme/mail/key.pem";
231 recipientDelimiter = "+";
232 masterConfig = {
233 submissions = {
234 type = "inet";
235 private = false;
236 command = "smtpd";
237 args = ["-o" "smtpd_tls_wrappermode=yes" ] ++ (let
238 mkKeyVal = opt: val: [ "-o" (opt + "=" + val) ];
239 in lib.concatLists (lib.mapAttrsToList mkKeyVal config.services.postfix.submissionOptions)
240 );
241 };
242 dovecot = {
243 type = "unix";
244 privileged = true;
245 chroot = false;
246 command = "pipe";
247 args = let
248 # rspamd could be used as a milter, but then it cannot apply
249 # its checks "per user" (milter is not yet dispatched to
250 # users), so we wrap dovecot-lda inside rspamc per recipient
251 # here.
252 dovecot_exe = "${pkgs.dovecot}/libexec/dovecot/dovecot-lda -f \${sender} -a \${original_recipient} -d \${user}@\${nexthop}";
253 in [
254 "flags=DRhu" "user=vhost:vhost"
255 "argv=${pkgs.rspamd}/bin/rspamc -h ${config.myServices.mail.rspamd.sockets.worker-controller} -c bayes -d \${user}@\${nexthop} --mime --exec {${dovecot_exe}}"
256 ];
257 };
258 };
257 }; 259 };
258 }; 260 security.acme.certs."mail" = {
259 config.security.acme.certs."mail" = { 261 postRun = ''
260 postRun = '' 262 systemctl restart postfix.service
261 systemctl restart postfix.service 263 '';
262 ''; 264 extraDomains = {
263 extraDomains = { 265 "smtp.immae.eu" = null;
264 "smtp.immae.eu" = null; 266 };
265 }; 267 };
266 }; 268 };
267} 269}
diff --git a/modules/private/mail/rspamd.nix b/modules/private/mail/rspamd.nix
index af3541f..5e0a239 100644
--- a/modules/private/mail/rspamd.nix
+++ b/modules/private/mail/rspamd.nix
@@ -10,78 +10,80 @@
10 rspamd sockets 10 rspamd sockets
11 ''; 11 '';
12 }; 12 };
13 config.services.backup.profiles.mail.excludeFile = '' 13 config = lib.mkIf config.myServices.mail.enable {
14 + /var/lib/rspamd 14 services.backup.profiles.mail.excludeFile = ''
15 ''; 15 + /var/lib/rspamd
16 config.services.cron.systemCronJobs = let
17 cron_script = pkgs.runCommand "cron_script" {
18 buildInputs = [ pkgs.makeWrapper ];
19 } ''
20 mkdir -p $out
21 cp ${./scan_reported_mails} $out/scan_reported_mails
22 patchShebangs $out
23 for i in $out/*; do
24 wrapProgram "$i" --prefix PATH : ${lib.makeBinPath [ pkgs.coreutils pkgs.rspamd pkgs.flock ]}
25 done
26 ''; 16 '';
27 in 17 services.cron.systemCronJobs = let
28 [ "*/20 * * * * vhost ${cron_script}/scan_reported_mails" ]; 18 cron_script = pkgs.runCommand "cron_script" {
29 19 buildInputs = [ pkgs.makeWrapper ];
30 config.services.rspamd = { 20 } ''
31 enable = true; 21 mkdir -p $out
32 debug = true; 22 cp ${./scan_reported_mails} $out/scan_reported_mails
33 overrides = { 23 patchShebangs $out
34 "actions.conf".text = '' 24 for i in $out/*; do
35 reject = null; 25 wrapProgram "$i" --prefix PATH : ${lib.makeBinPath [ pkgs.coreutils pkgs.rspamd pkgs.flock ]}
36 add_header = 6; 26 done
37 greylist = null;
38 ''; 27 '';
39 "milter_headers.conf".text = '' 28 in
40 extended_spam_headers = true; 29 [ "*/20 * * * * vhost ${cron_script}/scan_reported_mails" ];
41 ''; 30
42 }; 31 services.rspamd = {
43 locals = { 32 enable = true;
44 "redis.conf".text = '' 33 debug = true;
45 servers = "${myconfig.env.mail.rspamd.redis.socket}"; 34 overrides = {
46 db = "${myconfig.env.mail.rspamd.redis.db}"; 35 "actions.conf".text = ''
36 reject = null;
37 add_header = 6;
38 greylist = null;
39 '';
40 "milter_headers.conf".text = ''
41 extended_spam_headers = true;
47 ''; 42 '';
48 "classifier-bayes.conf".text = '' 43 };
49 users_enabled = true; 44 locals = {
50 backend = "redis"; 45 "redis.conf".text = ''
51 servers = "${myconfig.env.mail.rspamd.redis.socket}"; 46 servers = "${myconfig.env.mail.rspamd.redis.socket}";
52 database = "${myconfig.env.mail.rspamd.redis.db}"; 47 db = "${myconfig.env.mail.rspamd.redis.db}";
53 autolearn = true; 48 '';
54 cache { 49 "classifier-bayes.conf".text = ''
50 users_enabled = true;
55 backend = "redis"; 51 backend = "redis";
56 } 52 servers = "${myconfig.env.mail.rspamd.redis.socket}";
57 new_schema = true; 53 database = "${myconfig.env.mail.rspamd.redis.db}";
58 statfile { 54 autolearn = true;
59 BAYES_HAM { 55 cache {
60 spam = false; 56 backend = "redis";
61 } 57 }
62 BAYES_SPAM { 58 new_schema = true;
63 spam = true; 59 statfile {
60 BAYES_HAM {
61 spam = false;
62 }
63 BAYES_SPAM {
64 spam = true;
65 }
64 } 66 }
65 } 67 '';
66 ''; 68 };
67 }; 69 workers = {
68 workers = { 70 controller = {
69 controller = { 71 extraConfig = ''
70 extraConfig = '' 72 enable_password = "${myconfig.env.mail.rspamd.write_password_hashed}";
71 enable_password = "${myconfig.env.mail.rspamd.write_password_hashed}"; 73 password = "${myconfig.env.mail.rspamd.read_password_hashed}";
72 password = "${myconfig.env.mail.rspamd.read_password_hashed}"; 74 '';
73 ''; 75 bindSockets = [ {
74 bindSockets = [ { 76 socket = config.myServices.mail.rspamd.sockets.worker-controller;
75 socket = config.myServices.mail.rspamd.sockets.worker-controller; 77 mode = "0660";
76 mode = "0660"; 78 owner = config.services.rspamd.user;
77 owner = config.services.rspamd.user; 79 group = "vhost";
78 group = "vhost"; 80 } ];
79 } ]; 81 };
82 };
83 postfix = {
84 enable = true;
85 config = {};
80 }; 86 };
81 };
82 postfix = {
83 enable = true;
84 config = {};
85 }; 87 };
86 }; 88 };
87} 89}
diff --git a/modules/private/mpd.nix b/modules/private/mpd.nix
index b224165..759c9d3 100644
--- a/modules/private/mpd.nix
+++ b/modules/private/mpd.nix
@@ -1,6 +1,7 @@
1{ lib, pkgs, config, myconfig, ... }: 1{ lib, pkgs, config, myconfig, ... }:
2{ 2{
3 config = { 3 options.myServices.mpd.enable = lib.mkEnableOption "enable MPD";
4 config = lib.mkIf config.myServices.mpd.enable {
4 services.backup.profiles.mpd = { 5 services.backup.profiles.mpd = {
5 rootDir = "/var/lib/mpd"; 6 rootDir = "/var/lib/mpd";
6 }; 7 };
diff --git a/modules/private/system/backup-2.nix b/modules/private/system/backup-2.nix
new file mode 100644
index 0000000..c67eab6
--- /dev/null
+++ b/modules/private/system/backup-2.nix
@@ -0,0 +1,24 @@
1{ privateFiles }:
2{ config, pkgs, myconfig, ... }:
3{
4 boot.kernelPackages = pkgs.linuxPackages_latest;
5 _module.args.privateFiles = privateFiles;
6 imports = builtins.attrValues (import ../..);
7
8 deployment = {
9 targetEnv = "hetznerCloud";
10 hetznerCloud = {
11 authToken = myconfig.env.hetznerCloud.authToken;
12 datacenter = "hel1-dc2";
13 location ="hel1";
14 serverType = "cx11";
15 };
16 };
17
18 # This value determines the NixOS release with which your system is
19 # to be compatible, in order to avoid breaking some software such as
20 # database servers. You should change this only after NixOS release
21 # notes say you should.
22 # https://nixos.org/nixos/manual/release-notes.html
23 system.stateVersion = "19.03"; # Did you read the comment?
24}
diff --git a/modules/private/system/eldiron.nix b/modules/private/system/eldiron.nix
index 22de37e..079216b 100644
--- a/modules/private/system/eldiron.nix
+++ b/modules/private/system/eldiron.nix
@@ -28,7 +28,13 @@
28 myServices.irc.enable = true; 28 myServices.irc.enable = true;
29 myServices.pub.enable = true; 29 myServices.pub.enable = true;
30 myServices.tasks.enable = true; 30 myServices.tasks.enable = true;
31 myServices.mpd.enable = true;
32 myServices.dns.enable = true;
33 myServices.certificates.enable = true;
34 myServices.websites.enable = true;
35 myServices.mail.enable = true;
31 services.pure-ftpd.enable = true; 36 services.pure-ftpd.enable = true;
37 services.backup.enable = true;
32 38
33 deployment = { 39 deployment = {
34 targetEnv = "hetzner"; 40 targetEnv = "hetzner";
diff --git a/modules/private/tasks/default.nix b/modules/private/tasks/default.nix
index b2191c0..88d3b7a 100644
--- a/modules/private/tasks/default.nix
+++ b/modules/private/tasks/default.nix
@@ -192,7 +192,7 @@ in {
192 192
193 myServices.websites.webappDirs._task = ./www; 193 myServices.websites.webappDirs._task = ./www;
194 194
195 security.acme.certs."task" = config.services.myCertificates.certConfig // { 195 security.acme.certs."task" = config.myServices.certificates.certConfig // {
196 inherit user group; 196 inherit user group;
197 plugins = [ "fullchain.pem" "key.pem" "cert.pem" "account_key.json" ]; 197 plugins = [ "fullchain.pem" "key.pem" "cert.pem" "account_key.json" ];
198 domain = fqdn; 198 domain = fqdn;
diff --git a/modules/private/websites/default.nix b/modules/private/websites/default.nix
index e2bcef5..119d62e 100644
--- a/modules/private/websites/default.nix
+++ b/modules/private/websites/default.nix
@@ -64,15 +64,19 @@ let
64 makeExtraConfig = (builtins.filter (x: x != null) (lib.attrsets.mapAttrsToList (n: v: v.extraConfig or null) apacheConfig)); 64 makeExtraConfig = (builtins.filter (x: x != null) (lib.attrsets.mapAttrsToList (n: v: v.extraConfig or null) apacheConfig));
65in 65in
66{ 66{
67 options.myServices.websites.webappDirs = lib.mkOption { 67 options.myServices.websites = {
68 type = lib.types.attrsOf lib.types.path; 68 enable = lib.mkEnableOption "enable websites";
69 description = '' 69
70 Webapp paths to create in /run/current-system/webapps 70 webappDirs = lib.mkOption {
71 ''; 71 type = lib.types.attrsOf lib.types.path;
72 default = {}; 72 description = ''
73 Webapp paths to create in /run/current-system/webapps
74 '';
75 default = {};
76 };
73 }; 77 };
74 78
75 config = { 79 config = lib.mkIf config.myServices.websites.enable {
76 services.backup.profiles.php = { 80 services.backup.profiles.php = {
77 rootDir = "/var/lib/php"; 81 rootDir = "/var/lib/php";
78 }; 82 };
diff --git a/modules/private/websites/tools/mail/mta-sts.nix b/modules/private/websites/tools/mail/mta-sts.nix
index bedefda..d443f55 100644
--- a/modules/private/websites/tools/mail/mta-sts.nix
+++ b/modules/private/websites/tools/mail/mta-sts.nix
@@ -28,28 +28,30 @@ let
28 "cp ${file d} $out/${d.domain}.txt" 28 "cp ${file d} $out/${d.domain}.txt"
29 ) domains)} 29 ) domains)}
30 ''; 30 '';
31 cfg = config.myServices.websites.tools.email;
31in 32in
32{ 33{
33 config.myServices.websites.webappDirs = { 34 config = lib.mkIf cfg.enable {
34 _mta-sts = root; 35 myServices.websites.webappDirs = {
35 }; 36 _mta-sts = root;
37 };
36 38
37 config.services.websites.env.tools.vhostConfs.mta_sts = { 39 services.websites.env.tools.vhostConfs.mta_sts = {
38 certName = "mail"; 40 certName = "mail";
39 addToCerts = true; 41 addToCerts = true;
40 hosts = ["mta-sts.mail.immae.eu"] ++ map (v: "mta-sts.${v.domain}") domains; 42 hosts = ["mta-sts.mail.immae.eu"] ++ map (v: "mta-sts.${v.domain}") domains;
41 root = "/run/current-system/webapps/_mta-sts"; 43 root = "/run/current-system/webapps/_mta-sts";
42 extraConfig = [ 44 extraConfig = [
43 '' 45 ''
44 RewriteEngine on 46 RewriteEngine on
45 RewriteCond %{HTTP_HOST} ^mta-sts.(.*)$ 47 RewriteCond %{HTTP_HOST} ^mta-sts.(.*)$
46 RewriteRule ^/.well-known/mta-sts.txt$ %{DOCUMENT_ROOT}/%1.txt [L] 48 RewriteRule ^/.well-known/mta-sts.txt$ %{DOCUMENT_ROOT}/%1.txt [L]
47 <Directory /run/current-system/webapps/_mta-sts> 49 <Directory /run/current-system/webapps/_mta-sts>
48 Require all granted 50 Require all granted
49 Options -Indexes 51 Options -Indexes
50 </Directory> 52 </Directory>
51 '' 53 ''
52 ]; 54 ];
55 };
53 }; 56 };
54
55} 57}
diff --git a/nixops/default.nix b/nixops/default.nix
index 649e431..f65f3da 100644
--- a/nixops/default.nix
+++ b/nixops/default.nix
@@ -5,5 +5,7 @@
5 enableRollback = true; 5 enableRollback = true;
6 }; 6 };
7 7
8 resources.sshKeyPairs.ssh-key = {};
8 eldiron = import ../modules/private/system/eldiron.nix { inherit privateFiles; }; 9 eldiron = import ../modules/private/system/eldiron.nix { inherit privateFiles; };
10 backup-2 = import ../modules/private/system/backup-2.nix { inherit privateFiles; };
9} 11}
diff --git a/overlays/environments/immae-eu.nix b/overlays/environments/immae-eu.nix
index db1caa4..cc2e5c3 100644
--- a/overlays/environments/immae-eu.nix
+++ b/overlays/environments/immae-eu.nix
@@ -63,7 +63,7 @@ let
63 newsboat irssi 63 newsboat irssi
64 64
65 # nix 65 # nix
66 mylibs.yarn2nixPackage.yarn2nix 66 mylibs.yarn2nixPackage.yarn2nix nix
67 nixops nix-prefetch-scripts nix-generate-from-cpan 67 nixops nix-prefetch-scripts nix-generate-from-cpan
68 nix-zsh-completions bundix nodePackages.bower2nix 68 nix-zsh-completions bundix nodePackages.bower2nix
69 nodePackages.node2nix 69 nodePackages.node2nix
diff --git a/overlays/nixops/default.nix b/overlays/nixops/default.nix
index eb29ecd..247d036 100644
--- a/overlays/nixops/default.nix
+++ b/overlays/nixops/default.nix
@@ -1,5 +1,6 @@
1self: super: { 1self: super: {
2 nixops = super.nixops.overrideAttrs (old: { 2 nixops = super.nixops.overrideAttrs (old: {
3 patches = [ ./hetzner_cloud.patch ];
3 preConfigure = (old.preConfigure or "") + '' 4 preConfigure = (old.preConfigure or "") + ''
4 sed -i -e "/'keyFile'/s/'path'/'string'/" nixops/backends/__init__.py 5 sed -i -e "/'keyFile'/s/'path'/'string'/" nixops/backends/__init__.py
5 ''; 6 '';
diff --git a/overlays/nixops/hetzner_cloud.patch b/overlays/nixops/hetzner_cloud.patch
new file mode 100644
index 0000000..b75c116
--- /dev/null
+++ b/overlays/nixops/hetzner_cloud.patch
@@ -0,0 +1,480 @@
1From 272e50d0b0262e49cdcaad42cdab57aad183d1c2 Mon Sep 17 00:00:00 2001
2From: goodraven
3 <employee-pseudonym-7f597def-7eeb-47f8-b10a-0724f2ba59a9@google.com>
4Date: Thu, 3 May 2018 22:24:58 -0700
5Subject: [PATCH] Initial commit adding support for hetzner cloud
6
7This is based on the digital ocean backend. It also uses nixos-infect. I extended nixos-infect to be generic
8for both backends.
9
10Fixes #855
11---
12 examples/trivial-hetzner-cloud.nix | 12 ++
13 nix/eval-machine-info.nix | 1 +
14 nix/hetzner-cloud.nix | 56 +++++++
15 nix/options.nix | 1 +
16 nixops/backends/hetzner_cloud.py | 230 +++++++++++++++++++++++++++++
17 nixops/data/nixos-infect | 77 +++++++---
18 6 files changed, 354 insertions(+), 23 deletions(-)
19 create mode 100644 examples/trivial-hetzner-cloud.nix
20 create mode 100644 nix/hetzner-cloud.nix
21 create mode 100644 nixops/backends/hetzner_cloud.py
22
23diff --git a/examples/trivial-hetzner-cloud.nix b/examples/trivial-hetzner-cloud.nix
24new file mode 100644
25index 000000000..c61add6bb
26--- /dev/null
27+++ b/examples/trivial-hetzner-cloud.nix
28@@ -0,0 +1,12 @@
29+{
30+ resources.sshKeyPairs.ssh-key = {};
31+
32+ machine = { config, pkgs, ... }: {
33+ services.openssh.enable = true;
34+
35+ deployment.targetEnv = "hetznerCloud";
36+ deployment.hetznerCloud.serverType = "cx11";
37+
38+ networking.firewall.allowedTCPPorts = [ 22 ];
39+ };
40+}
41diff --git a/nix/eval-machine-info.nix b/nix/eval-machine-info.nix
42index 2884b4b47..6a7205786 100644
43--- a/nix/eval-machine-info.nix
44+++ b/nix/eval-machine-info.nix
45@@ -309,6 +309,7 @@ rec {
46 digitalOcean = optionalAttrs (v.config.deployment.targetEnv == "digitalOcean") v.config.deployment.digitalOcean;
47 gce = optionalAttrs (v.config.deployment.targetEnv == "gce") v.config.deployment.gce;
48 hetzner = optionalAttrs (v.config.deployment.targetEnv == "hetzner") v.config.deployment.hetzner;
49+ hetznerCloud = optionalAttrs (v.config.deployment.targetEnv == "hetznerCloud") v.config.deployment.hetznerCloud;
50 container = optionalAttrs (v.config.deployment.targetEnv == "container") v.config.deployment.container;
51 route53 = v.config.deployment.route53;
52 virtualbox =
53diff --git a/nix/hetzner-cloud.nix b/nix/hetzner-cloud.nix
54new file mode 100644
55index 000000000..21d148c1a
56--- /dev/null
57+++ b/nix/hetzner-cloud.nix
58@@ -0,0 +1,56 @@
59+{ config, pkgs, lib, utils, ... }:
60+
61+with utils;
62+with lib;
63+with import ./lib.nix lib;
64+
65+let
66+ cfg = config.deployment.hetznerCloud;
67+in
68+{
69+ ###### interface
70+ options = {
71+
72+ deployment.hetznerCloud.authToken = mkOption {
73+ default = "";
74+ example = "8b2f4e96af3997853bfd4cd8998958eab871d9614e35d63fab45a5ddf981c4da";
75+ type = types.str;
76+ description = ''
77+ The API auth token. We're checking the environment for
78+ <envar>HETZNER_CLOUD_AUTH_TOKEN</envar> first and if that is
79+ not set we try this auth token.
80+ '';
81+ };
82+
83+ deployment.hetznerCloud.datacenter = mkOption {
84+ example = "fsn1-dc8";
85+ default = null;
86+ type = types.nullOr types.str;
87+ description = ''
88+ The datacenter.
89+ '';
90+ };
91+
92+ deployment.hetznerCloud.location = mkOption {
93+ example = "fsn1";
94+ default = null;
95+ type = types.nullOr types.str;
96+ description = ''
97+ The location.
98+ '';
99+ };
100+
101+ deployment.hetznerCloud.serverType = mkOption {
102+ example = "cx11";
103+ type = types.str;
104+ description = ''
105+ Name or id of server types.
106+ '';
107+ };
108+ };
109+
110+ config = mkIf (config.deployment.targetEnv == "hetznerCloud") {
111+ nixpkgs.system = mkOverride 900 "x86_64-linux";
112+ services.openssh.enable = true;
113+ };
114+}
115diff --git a/nix/options.nix b/nix/options.nix
116index 0866c3ab8..db021f74d 100644
117--- a/nix/options.nix
118+++ b/nix/options.nix
119@@ -22,6 +22,7 @@ in
120 ./keys.nix
121 ./gce.nix
122 ./hetzner.nix
123+ ./hetzner-cloud.nix
124 ./container.nix
125 ./libvirtd.nix
126 ];
127diff --git a/nixops/backends/hetzner_cloud.py b/nixops/backends/hetzner_cloud.py
128new file mode 100644
129index 000000000..a2cb176b9
130--- /dev/null
131+++ b/nixops/backends/hetzner_cloud.py
132@@ -0,0 +1,230 @@
133+# -*- coding: utf-8 -*-
134+"""
135+A backend for hetzner cloud.
136+
137+This backend uses nixos-infect (which uses nixos LUSTRATE) to infect a
138+hetzner cloud instance. The setup requires two reboots, one for
139+the infect itself, another after we pushed the nixos image.
140+"""
141+import os
142+import os.path
143+import time
144+import socket
145+
146+import requests
147+
148+import nixops.resources
149+from nixops.backends import MachineDefinition, MachineState
150+from nixops.nix_expr import Function, RawValue
151+import nixops.util
152+import nixops.known_hosts
153+
154+infect_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'data', 'nixos-infect'))
155+
156+API_HOST = 'api.hetzner.cloud'
157+
158+class ApiError(Exception):
159+ pass
160+
161+class ApiNotFoundError(ApiError):
162+ pass
163+
164+class HetznerCloudDefinition(MachineDefinition):
165+ @classmethod
166+ def get_type(cls):
167+ return "hetznerCloud"
168+
169+ def __init__(self, xml, config):
170+ MachineDefinition.__init__(self, xml, config)
171+ self.auth_token = config["hetznerCloud"]["authToken"]
172+ self.location = config["hetznerCloud"]["location"]
173+ self.datacenter = config["hetznerCloud"]["datacenter"]
174+ self.server_type = config["hetznerCloud"]["serverType"]
175+
176+ def show_type(self):
177+ return "{0} [{1}]".format(self.get_type(), self.location or self.datacenter or 'any location')
178+
179+
180+class HetznerCloudState(MachineState):
181+ @classmethod
182+ def get_type(cls):
183+ return "hetznerCloud"
184+
185+ state = nixops.util.attr_property("state", MachineState.MISSING, int) # override
186+ public_ipv4 = nixops.util.attr_property("publicIpv4", None)
187+ public_ipv6 = nixops.util.attr_property("publicIpv6", None)
188+ location = nixops.util.attr_property("hetznerCloud.location", None)
189+ datacenter = nixops.util.attr_property("hetznerCloud.datacenter", None)
190+ server_type = nixops.util.attr_property("hetznerCloud.serverType", None)
191+ auth_token = nixops.util.attr_property("hetznerCloud.authToken", None)
192+ server_id = nixops.util.attr_property("hetznerCloud.serverId", None, int)
193+
194+ def __init__(self, depl, name, id):
195+ MachineState.__init__(self, depl, name, id)
196+ self.name = name
197+
198+ def get_ssh_name(self):
199+ return self.public_ipv4
200+
201+ def get_ssh_flags(self, *args, **kwargs):
202+ super_flags = super(HetznerCloudState, self).get_ssh_flags(*args, **kwargs)
203+ return super_flags + [
204+ '-o', 'UserKnownHostsFile=/dev/null',
205+ '-o', 'StrictHostKeyChecking=no',
206+ '-i', self.get_ssh_private_key_file(),
207+ ]
208+
209+ def get_physical_spec(self):
210+ return Function("{ ... }", {
211+ 'imports': [ RawValue('<nixpkgs/nixos/modules/profiles/qemu-guest.nix>') ],
212+ ('boot', 'loader', 'grub', 'device'): 'nodev',
213+ ('fileSystems', '/'): { 'device': '/dev/sda1', 'fsType': 'ext4'},
214+ ('users', 'extraUsers', 'root', 'openssh', 'authorizedKeys', 'keys'): [self.depl.active_resources.get('ssh-key').public_key],
215+ })
216+
217+ def get_ssh_private_key_file(self):
218+ return self.write_ssh_private_key(self.depl.active_resources.get('ssh-key').private_key)
219+
220+ def create_after(self, resources, defn):
221+ # make sure the ssh key exists before we do anything else
222+ return {
223+ r for r in resources if
224+ isinstance(r, nixops.resources.ssh_keypair.SSHKeyPairState)
225+ }
226+
227+ def get_auth_token(self):
228+ return os.environ.get('HETZNER_CLOUD_AUTH_TOKEN', self.auth_token)
229+
230+ def _api(self, path, method=None, data=None, json=True):
231+ """Basic wrapper around requests that handles auth and serialization."""
232+ assert path[0] == '/'
233+ url = 'https://%s%s' % (API_HOST, path)
234+ token = self.get_auth_token()
235+ if not token:
236+ raise Exception('No hetzner cloud auth token set')
237+ headers = {
238+ 'Authorization': 'Bearer '+self.get_auth_token(),
239+ }
240+ res = requests.request(
241+ method=method,
242+ url=url,
243+ json=data,
244+ headers=headers)
245+
246+ if res.status_code == 404:
247+ raise ApiNotFoundError('Not Found: %r' % path)
248+ elif not res.ok:
249+ raise ApiError('Response for %s %s has status code %d: %s' % (method, path, res.status_code, res.content))
250+ if not json:
251+ return
252+ try:
253+ res_data = res.json()
254+ except ValueError as e:
255+ raise ApiError('Response for %s %s has invalid JSON (%s): %r' % (method, path, e, res.content))
256+ return res_data
257+
258+
259+ def destroy(self, wipe=False):
260+ if not self.server_id:
261+ self.log('server {} was never made'.format(self.name))
262+ return
263+ self.log('destroying server {} with id {}'.format(self.name, self.server_id))
264+ try:
265+ res = self._api('/v1/servers/%s' % (self.server_id), method='DELETE')
266+ except ApiNotFoundError:
267+ self.log("server not found - assuming it's been destroyed already")
268+
269+ self.public_ipv4 = None
270+ self.server_id = None
271+
272+ return True
273+
274+ def _create_ssh_key(self, public_key):
275+ """Create or get an ssh key and return an id."""
276+ public_key = public_key.strip()
277+ res = self._api('/v1/ssh_keys', method='GET')
278+ name = 'nixops-%s-%s' % (self.depl.uuid, self.name)
279+ deletes = []
280+ for key in res['ssh_keys']:
281+ if key['public_key'].strip() == public_key:
282+ return key['id']
283+ if key['name'] == name:
284+ deletes.append(key['id'])
285+ for d in deletes:
286+ # This reply is empty, so don't decode json.
287+ self._api('/v1/ssh_keys/%d' % d, method='DELETE', json=False)
288+ res = self._api('/v1/ssh_keys', method='POST', data={
289+ 'name': name,
290+ 'public_key': public_key,
291+ })
292+ return res['ssh_key']['id']
293+
294+ def create(self, defn, check, allow_reboot, allow_recreate):
295+ ssh_key = self.depl.active_resources.get('ssh-key')
296+ if ssh_key is None:
297+ raise Exception('Please specify a ssh-key resource (resources.sshKeyPairs.ssh-key = {}).')
298+
299+ self.set_common_state(defn)
300+
301+ if self.server_id is not None:
302+ return
303+
304+ ssh_key_id = self._create_ssh_key(ssh_key.public_key)
305+
306+ req = {
307+ 'name': self.name,
308+ 'server_type': defn.server_type,
309+ 'start_after_create': True,
310+ 'image': 'debian-9',
311+ 'ssh_keys': [
312+ ssh_key_id,
313+ ],
314+ }
315+
316+ if defn.datacenter:
317+ req['datacenter'] = defn.datacenter
318+ elif defn.location:
319+ req['location'] = defn.location
320+
321+ self.log_start("creating server ...")
322+ create_res = self._api('/v1/servers', method='POST', data=req)
323+ self.server_id = create_res['server']['id']
324+ self.public_ipv4 = create_res['server']['public_net']['ipv4']['ip']
325+ self.public_ipv6 = create_res['server']['public_net']['ipv6']['ip']
326+ self.datacenter = create_res['server']['datacenter']['name']
327+ self.location = create_res['server']['datacenter']['location']['name']
328+
329+ action = create_res['action']
330+ action_path = '/v1/servers/%d/actions/%d' % (self.server_id, action['id'])
331+
332+ while action['status'] == 'running':
333+ time.sleep(1)
334+ res = self._api(action_path, method='GET')
335+ action = res['action']
336+
337+ if action['status'] != 'success':
338+ raise Exception('unexpected status: %s' % action['status'])
339+
340+ self.log_end("{}".format(self.public_ipv4))
341+
342+ self.wait_for_ssh()
343+ self.log_start("running nixos-infect")
344+ self.run_command('bash </dev/stdin 2>&1', stdin=open(infect_path))
345+ self.reboot_sync()
346+
347+ def reboot(self, hard=False):
348+ if hard:
349+ self.log("sending hard reset to server...")
350+ res = self._api('/v1/servers/%d/actions/reset' % self.server_id, method='POST')
351+ action = res['action']
352+ action_path = '/v1/servers/%d/actions/%d' % (self.server_id, action['id'])
353+ while action['status'] == 'running':
354+ time.sleep(1)
355+ res = self._api(action_path, method='GET')
356+ action = res['action']
357+ if action['status'] != 'success':
358+ raise Exception('unexpected status: %s' % action['status'])
359+ self.wait_for_ssh()
360+ self.state = self.STARTING
361+ else:
362+ MachineState.reboot(self, hard=hard)
363diff --git a/nixops/data/nixos-infect b/nixops/data/nixos-infect
364index 66634357b..437a2ec61 100644
365--- a/nixops/data/nixos-infect
366+++ b/nixops/data/nixos-infect
367@@ -68,26 +68,49 @@ makeConf() {
368 }
369 EOF
370 # (nixos-generate-config will add qemu-user and bind-mounts, so avoid)
371+ local disk
372+ if [ -e /dev/sda ]; then
373+ disk=/dev/sda
374+ else
375+ disk=/dev/vda
376+ fi
377 cat > /etc/nixos/hardware-configuration.nix << EOF
378 { ... }:
379 {
380 imports = [ <nixpkgs/nixos/modules/profiles/qemu-guest.nix> ];
381- boot.loader.grub.device = "/dev/vda";
382- fileSystems."/" = { device = "/dev/vda1"; fsType = "ext4"; };
383+ boot.loader.grub.device = "${disk}";
384+ fileSystems."/" = { device = "${disk}1"; fsType = "ext4"; };
385 }
386 EOF
387
388 local IFS=$'\n'
389- ens3_ip4s=($(ip address show dev eth0 | grep 'inet ' | sed -r 's|.*inet ([0-9.]+)/([0-9]+).*|{ address="\1"; prefixLength=\2; }|'))
390- ens3_ip6s=($(ip address show dev eth0 | grep 'inet6 .*global' | sed -r 's|.*inet6 ([0-9a-f:]+)/([0-9]+).*|{ address="\1"; prefixLength=\2; }|'))
391- ens4_ip4s=($(ip address show dev eth1 | grep 'inet ' | sed -r 's|.*inet ([0-9.]+)/([0-9]+).*|{ address="\1"; prefixLength=\2; }|'))
392- ens4_ip6s=($(ip address show dev eth1 | grep 'inet6 .*global' | sed -r 's|.*inet6 ([0-9a-f:]+)/([0-9]+).*|{ address="\1"; prefixLength=\2; }|'))
393- gateway=($(ip route show dev eth0 | grep default | sed -r 's|default via ([0-9.]+).*|\1|'))
394- gateway6=($(ip -6 route show dev eth0 | grep default | sed -r 's|default via ([0-9a-f:]+).*|\1|'))
395- ether0=($(ip address show dev eth0 | grep link/ether | sed -r 's|.*link/ether ([0-9a-f:]+) .*|\1|'))
396- ether1=($(ip address show dev eth1 | grep link/ether | sed -r 's|.*link/ether ([0-9a-f:]+) .*|\1|'))
397+ gateway=($(ip route show | grep default | sed -r 's|default via ([0-9.]+).*|\1|'))
398+ gateway6=($(ip -6 route show | grep default | sed -r 's|default via ([0-9a-f:]+).*|\1|'))
399+ interfaces=($(ip link | awk -F ': ' '/^[0-9]*: / {if ($2 != "lo") {print $2}}'))
400 nameservers=($(grep ^nameserver /etc/resolv.conf | cut -f2 -d' '))
401
402+ # Predict the predictable name for each interface since that is enabled in
403+ # the nixos system.
404+ declare -A predictable_names
405+ for interface in ${interfaces[@]}; do
406+ # udevadm prints out the candidate names which will be selected if
407+ # available in this order.
408+ local name=$(udevadm info /sys/class/net/$interface | awk -F = '
409+ /^E: ID_NET_NAME_FROM_DATABASE=/ {arr[1]=$2}
410+ /^E: ID_NET_NAME_ONBOARD=/ {arr[2]=$2}
411+ /^E: ID_NET_NAME_SLOT=/ {arr[3]=$2}
412+ /^E: ID_NET_NAME_PATH=/ {arr[4]=$2}
413+ /^E: ID_NET_NAME_MAC=/ {arr[5]=$2}
414+ END {for (i=1;i<6;i++) {if (length(arr[i]) > 0) { print arr[i]; break}}}')
415+ if [ -z "$name" ]; then
416+ echo Could not determine predictable name for interface $interface
417+ fi
418+ predictable_names[$interface]=$name
419+ done
420+
421+ # Take a gamble on the first interface being able to reach the gateway.
422+ local default_interface=${predictable_names[${interfaces[0]}]}
423+
424 cat > /etc/nixos/networking.nix << EOF
425 { ... }: {
426 # This file was populated at runtime with the networking
427@@ -96,25 +119,27 @@ EOF
428 nameservers = [$(for a in ${nameservers[@]}; do echo -n "
429 \"$a\""; done)
430 ];
431- defaultGateway = "${gateway}";
432- defaultGateway6 = "${gateway6}";
433+ defaultGateway = {address = "${gateway}"; interface = "${default_interface}";};
434+ defaultGateway6 = {address = "${gateway6}"; interface = "${default_interface}";};
435 interfaces = {
436- ens3 = {
437- ip4 = [$(for a in ${ens3_ip4s[@]}; do echo -n "
438- $a"; done)
439- ];
440- ip6 = [$(for a in ${ens3_ip6s[@]}; do echo -n "
441- $a"; done)
442- ];
443- };
444- ens4 = {
445- ip4 = [$(for a in ${ens4_ip4s[@]}; do echo -n "
446+EOF
447+
448+ for interface in ${interfaces[@]}; do
449+ ip4s=($(ip address show dev $interface | grep 'inet ' | sed -r 's|.*inet ([0-9.]+)/([0-9]+).*|{ address="\1"; prefixLength=\2; }|'))
450+ ip6s=($(ip address show dev $interface | grep 'inet6 .*global' | sed -r 's|.*inet6 ([0-9a-f:]+)/([0-9]+).*|{ address="\1"; prefixLength=\2; }|'))
451+ cat >> /etc/nixos/networking.nix << EOF
452+ ${predictable_names[$interface]} = {
453+ ip4 = [$(for a in ${ip4s[@]}; do echo -n "
454 $a"; done)
455 ];
456- ip6 = [$(for a in ${ens4_ip6s[@]}; do echo -n "
457+ ip6 = [$(for a in ${ip6s[@]}; do echo -n "
458 $a"; done)
459 ];
460 };
461+EOF
462+ done
463+
464+ cat >> /etc/nixos/networking.nix << EOF
465 };
466 };
467 }
468@@ -154,6 +179,12 @@ export HOME="/root"
469 groupadd -r nixbld -g 30000
470 seq 1 10 | xargs -I{} useradd -c "Nix build user {}" -d /var/empty -g nixbld -G nixbld -M -N -r -s `which nologin` nixbld{}
471
472+if ! which curl >/dev/null 2>/dev/null; then
473+ if which apt-get >/dev/null 2>/dev/null; then
474+ apt-get update && apt-get install -y curl
475+ fi
476+fi
477+
478 curl https://nixos.org/nix/install | sh
479
480 source ~/.nix-profile/etc/profile.d/nix.sh