From 50a462a068d4113a677f0a9b683272b2ca9fa149 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Thu, 13 Feb 2020 22:27:59 +0100
Subject: [PATCH 01/37] Initial commit

---
 nextcloud/README.md                    | 29 +++++++++++++++++++++
 nextcloud/fpm.conf                     | 16 ++++++++++++
 nextcloud/nextcloud.conf.php           | 11 ++++++++
 nextcloud/nginx.conf                   | 36 ++++++++++++++++++++++++++
 nextcloud/redis.conf                   |  2 ++
 nginx/README.md                        | 10 +++++++
 nginx/sites-available/0nohost          |  6 +++++
 nginx/sites-available/redirect-ssl-all |  8 ++++++
 nginx/snippets/hsts                    |  1 +
 nginx/snippets/letsencrypt             |  3 +++
 nginx/snippets/redirect-ssl            |  3 +++
 ssh/README.md                          | 14 ++++++++++
 ssh/sshd_config                        | 10 +++++++
 13 files changed, 149 insertions(+)
 create mode 100644 nextcloud/README.md
 create mode 100644 nextcloud/fpm.conf
 create mode 100644 nextcloud/nextcloud.conf.php
 create mode 100644 nextcloud/nginx.conf
 create mode 100644 nextcloud/redis.conf
 create mode 100644 nginx/README.md
 create mode 100644 nginx/sites-available/0nohost
 create mode 100644 nginx/sites-available/redirect-ssl-all
 create mode 100644 nginx/snippets/hsts
 create mode 100644 nginx/snippets/letsencrypt
 create mode 100644 nginx/snippets/redirect-ssl
 create mode 100644 ssh/README.md
 create mode 100644 ssh/sshd_config

diff --git a/nextcloud/README.md b/nextcloud/README.md
new file mode 100644
index 0000000..a1e4637
--- /dev/null
+++ b/nextcloud/README.md
@@ -0,0 +1,29 @@
+# Nextcloud
+
+## Create user
+
+```sh
+echo 'cloud:*:3000:3000::/data/cloud:/bin/sh' | sudo tee -a /etc/passwd
+echo 'cloud:x:3000:' | sudo tee -a /etc/group
+sudo mkdir -p /data/cloud
+sudo chown cloud.cloud /data/cloud
+```
+
+## Install software
+
+```sh
+sudo apt install redis-server nginx php-redis php$VERSION-{cli,curl,fpm,gd,intl,mbstring,sqlite3,xml,zip}
+wget https://download.nextcloud.com/server/releases/latest.zip
+cd /data/cloud
+sudo -u cloud unzip ~/latest.zip
+```
+
+## Copy configuration
+
+```sh
+sudo cp nginx.conf /etc/nginx/sites-available/nextcloud
+sudo cp fpm.conf /etc/php/*/fpm/pool.d/nextcloud.conf
+cat redis.conf | sudo tee -a /etc/redis/redis.conf
+sudo -u cloud cp nextcloud.conf.php /data/cloud/nextcloud/config/local.config.php
+sudo sed -i s/example.com/$DOMAIN/g /etc/nginx/sites-available/nextcloud /data/cloud/nextcloud/config/local.config.php
+```
diff --git a/nextcloud/fpm.conf b/nextcloud/fpm.conf
new file mode 100644
index 0000000..0c01b9f
--- /dev/null
+++ b/nextcloud/fpm.conf
@@ -0,0 +1,16 @@
+[cloud]
+
+user = cloud
+group = cloud
+listen = /run/nextcloud.sock
+
+listen.owner = www-data
+listen.group = www-data
+
+env[PATH] = /usr/bin:/bin
+
+pm = ondemand
+pm.max_children = 5
+pm.max_requests = 500
+
+php_admin_value[memory_limit] = 512M
diff --git a/nextcloud/nextcloud.conf.php b/nextcloud/nextcloud.conf.php
new file mode 100644
index 0000000..554b3d4
--- /dev/null
+++ b/nextcloud/nextcloud.conf.php
@@ -0,0 +1,11 @@
+<?php $CONFIG = [
+	'trusted_domains' => [ 'cloud.example.com', '192.168.0.100:8080', 'localhost:8080' ],
+	'versions_retention_obligation' => 'auto, 2',
+	'trashbin_retention_obligation' => 'auto, 2',
+	'memcache.local' => '\OC\Memcache\Redis',
+	'memcache.distributed' => '\OC\Memcache\Redis',
+	'memcache.locking' => '\OC\Memcache\Redis',
+	'redis' => [ 'host' => '/run/redis/redis-server.sock', 'port' => 0 ],
+	'filesystem_check_changes' => 1,
+	'sqlite.journal_mode' => 'WAL',
+];
diff --git a/nextcloud/nginx.conf b/nextcloud/nginx.conf
new file mode 100644
index 0000000..d80611a
--- /dev/null
+++ b/nextcloud/nginx.conf
@@ -0,0 +1,36 @@
+server {
+	server_name cloud.example.com;
+
+	listen 443 ssl;
+
+	set $max_size 4G;
+
+	root /data/cloud/nextcloud;
+	client_max_body_size $max_size;
+
+	location / {
+		index index.php;
+		try_files $uri /index.php$request_uri;
+	}
+
+	location ~ ^/console\.php {
+		deny all;
+	}
+
+	location ~ ^(.+?\.php)(.*)$ {
+		fastcgi_split_path_info  ^(.+?\.php)(.*)$;
+		fastcgi_pass unix:/run/nextcloud.sock;
+		fastcgi_param PATH_INFO $fastcgi_path_info;
+		fastcgi_param front_controller_active true;
+		fastcgi_param PHP_VALUE "upload_max_filesize=$max_size\n post_max_size=$max_size";
+		include fastcgi.conf;
+	}
+
+	location = /.well-known/carddav {
+		return 301 $scheme://$host:$server_port/remote.php/dav;
+	}
+
+	location = /.well-known/caldav {
+		return 301 $scheme://$host:$server_port/remote.php/dav;
+	}
+}
diff --git a/nextcloud/redis.conf b/nextcloud/redis.conf
new file mode 100644
index 0000000..ed8ff96
--- /dev/null
+++ b/nextcloud/redis.conf
@@ -0,0 +1,2 @@
+unixsocket /run/redis/redis-server.sock
+unixsocketperm 777
diff --git a/nginx/README.md b/nginx/README.md
new file mode 100644
index 0000000..7a23a1b
--- /dev/null
+++ b/nginx/README.md
@@ -0,0 +1,10 @@
+# Nginx
+
+## Copy configuration
+
+```sh
+sudo cp -r s* /etc/nginx
+cd /etc/nginx/sites-enabled
+sudo ln -s ../sites-available/0nohost
+sudo ln -s ../sites-available/redirect-ssl-all
+```
diff --git a/nginx/sites-available/0nohost b/nginx/sites-available/0nohost
new file mode 100644
index 0000000..e6350ab
--- /dev/null
+++ b/nginx/sites-available/0nohost
@@ -0,0 +1,6 @@
+server {
+	listen 80 default_server;
+	listen 443 ssl default_server;
+	server_name _;
+	return 444;
+}
diff --git a/nginx/sites-available/redirect-ssl-all b/nginx/sites-available/redirect-ssl-all
new file mode 100644
index 0000000..823d16d
--- /dev/null
+++ b/nginx/sites-available/redirect-ssl-all
@@ -0,0 +1,8 @@
+server {
+	server_name .example.com;
+
+	listen 80;
+
+	include snippets/redirect-ssl;
+	include snippets/letsencrypt;
+}
diff --git a/nginx/snippets/hsts b/nginx/snippets/hsts
new file mode 100644
index 0000000..07baaf3
--- /dev/null
+++ b/nginx/snippets/hsts
@@ -0,0 +1 @@
+add_header Strict-Transport-Security max-age=31536000; # 1yr
diff --git a/nginx/snippets/letsencrypt b/nginx/snippets/letsencrypt
new file mode 100644
index 0000000..ac12fe2
--- /dev/null
+++ b/nginx/snippets/letsencrypt
@@ -0,0 +1,3 @@
+location /.well-known/acme-challenge {
+	alias /data/ssl/challenge;
+}
diff --git a/nginx/snippets/redirect-ssl b/nginx/snippets/redirect-ssl
new file mode 100644
index 0000000..385fb49
--- /dev/null
+++ b/nginx/snippets/redirect-ssl
@@ -0,0 +1,3 @@
+location / {
+	return 301 https://$host$request_uri;
+}
diff --git a/ssh/README.md b/ssh/README.md
new file mode 100644
index 0000000..13cbf3e
--- /dev/null
+++ b/ssh/README.md
@@ -0,0 +1,14 @@
+# SSH
+
+Use only one user `sshlogin` for logins to the server.
+Switch to your main user with `su - adminuser` afterwards.
+
+```sh
+echo 'sshlogin:x:999:65534::/home/sshlogin:/bin/sh' | sudo tee -a /etc/passwd
+sudo mkdir -p /home/sshlogin/.ssh
+sudo chown sshlogin.sshlogin /home/sshlogin/.ssh
+cat sshd_config | sudo tee -a /etc/ssh/sshd_config
+```
+
+* Either create a password with `sudo passwd sshlogin` or
+* Add a key `sudo -u sshlogin nano /home/sshlogin/.ssh/authorized_keys`
diff --git a/ssh/sshd_config b/ssh/sshd_config
new file mode 100644
index 0000000..4db04db
--- /dev/null
+++ b/ssh/sshd_config
@@ -0,0 +1,10 @@
+UseDNS no
+Port 22222
+AllowUsers sshlogin git backup-*
+ClientAliveInterval 10
+
+Match User backup-*
+	ForceCommand internal-sftp
+	ChrootDirectory %h
+	X11Forwarding no
+	AllowTcpForwarding no

From 25aeba25b3c32565342ef5cf3bc71a36f78e52c0 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Thu, 13 Feb 2020 23:01:34 +0100
Subject: [PATCH 02/37] Update

---
 nextcloud/README.md  | 14 +++++++++++++-
 nextcloud/crontab    |  1 +
 nextcloud/nginx.conf |  2 +-
 nginx/README.md      |  1 +
 restic/README.md     | 20 ++++++++++++++++++++
 restic/crontab       |  2 ++
 ssh/README.md        |  3 ++-
 7 files changed, 40 insertions(+), 3 deletions(-)
 create mode 100644 nextcloud/crontab
 create mode 100644 restic/README.md
 create mode 100644 restic/crontab

diff --git a/nextcloud/README.md b/nextcloud/README.md
index a1e4637..bdb6bc9 100644
--- a/nextcloud/README.md
+++ b/nextcloud/README.md
@@ -5,14 +5,23 @@
 ```sh
 echo 'cloud:*:3000:3000::/data/cloud:/bin/sh' | sudo tee -a /etc/passwd
 echo 'cloud:x:3000:' | sudo tee -a /etc/group
+
 sudo mkdir -p /data/cloud
 sudo chown cloud.cloud /data/cloud
 ```
 
+## Variables
+
+```sh
+PHPVERSION=x.x
+DOMAIN=example.com
+```
+
 ## Install software
 
 ```sh
-sudo apt install redis-server nginx php-redis php$VERSION-{cli,curl,fpm,gd,intl,mbstring,sqlite3,xml,zip}
+sudo apt install redis-server nginx php-redis php$PHPVERSION-{cli,curl,fpm,gd,intl,mbstring,sqlite3,xml,zip}
+
 wget https://download.nextcloud.com/server/releases/latest.zip
 cd /data/cloud
 sudo -u cloud unzip ~/latest.zip
@@ -24,6 +33,9 @@ sudo -u cloud unzip ~/latest.zip
 sudo cp nginx.conf /etc/nginx/sites-available/nextcloud
 sudo cp fpm.conf /etc/php/*/fpm/pool.d/nextcloud.conf
 cat redis.conf | sudo tee -a /etc/redis/redis.conf
+
 sudo -u cloud cp nextcloud.conf.php /data/cloud/nextcloud/config/local.config.php
+sudo -u cloud crontab crontab
+
 sudo sed -i s/example.com/$DOMAIN/g /etc/nginx/sites-available/nextcloud /data/cloud/nextcloud/config/local.config.php
 ```
diff --git a/nextcloud/crontab b/nextcloud/crontab
new file mode 100644
index 0000000..a0fe9ba
--- /dev/null
+++ b/nextcloud/crontab
@@ -0,0 +1 @@
+*/5 *	* * *	php nextcloud/cron.php
diff --git a/nextcloud/nginx.conf b/nextcloud/nginx.conf
index d80611a..15307c4 100644
--- a/nextcloud/nginx.conf
+++ b/nextcloud/nginx.conf
@@ -6,7 +6,7 @@ server {
 	set $max_size 4G;
 
 	root /data/cloud/nextcloud;
-	client_max_body_size $max_size;
+	client_max_body_size 0;
 
 	location / {
 		index index.php;
diff --git a/nginx/README.md b/nginx/README.md
index 7a23a1b..07cce4f 100644
--- a/nginx/README.md
+++ b/nginx/README.md
@@ -4,6 +4,7 @@
 
 ```sh
 sudo cp -r s* /etc/nginx
+
 cd /etc/nginx/sites-enabled
 sudo ln -s ../sites-available/0nohost
 sudo ln -s ../sites-available/redirect-ssl-all
diff --git a/restic/README.md b/restic/README.md
new file mode 100644
index 0000000..2590eed
--- /dev/null
+++ b/restic/README.md
@@ -0,0 +1,20 @@
+# Restic backups
+
+Download binary:
+https://github.com/restic/restic/releases/latest
+
+```sh
+bunzip2 restic*.bz2
+sudo cp restic* /usr/local/bin/restic
+
+echo 'nice /usr/local/bin/restic -r REPO -p /usr/local/share/key "$@"' | sudo tee /usr/local/bin/restic-cmd
+sudo chmod +x /usr/local/bin/restic-cmd
+sudo nano /usr/local/bin/restic-cmd
+
+cat /dev/urandom | base64 | head -c 64 | sudo tee /usr/local/share/key
+sudo chmod 0600 /usr/local/share/key
+
+sudo restic-cmd init
+sudo crontab crontab
+```
+
diff --git a/restic/crontab b/restic/crontab
new file mode 100644
index 0000000..cb74a6b
--- /dev/null
+++ b/restic/crontab
@@ -0,0 +1,2 @@
+48 *	* * *	/usr/local/bin/restic-cmd backup -q --exclude-if-present .nobackup /data
+18 3	* * *	/usr/local/bin/restic-cmd forget -q --keep-tag keep -H 24 -d 7 -m 24 -y 100
diff --git a/ssh/README.md b/ssh/README.md
index 13cbf3e..052e596 100644
--- a/ssh/README.md
+++ b/ssh/README.md
@@ -5,9 +5,10 @@ Switch to your main user with `su - adminuser` afterwards.
 
 ```sh
 echo 'sshlogin:x:999:65534::/home/sshlogin:/bin/sh' | sudo tee -a /etc/passwd
+cat sshd_config | sudo tee -a /etc/ssh/sshd_config
+
 sudo mkdir -p /home/sshlogin/.ssh
 sudo chown sshlogin.sshlogin /home/sshlogin/.ssh
-cat sshd_config | sudo tee -a /etc/ssh/sshd_config
 ```
 
 * Either create a password with `sudo passwd sshlogin` or

From 7dc51579f0247aee32768433ee48b85eb4fb76fc Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sat, 15 Feb 2020 23:06:27 +0100
Subject: [PATCH 03/37] postfix

---
 nextcloud/README.md  | 17 ++++++++++-------
 postfix/alias.cf     |  2 ++
 postfix/domains.cf   |  2 ++
 postfix/main.cf      | 33 +++++++++++++++++++++++++++++++++
 postfix/relay.cf     |  2 ++
 postfix/schema.sql   |  3 +++
 postfix/transport.cf |  2 ++
 postfix/virtual.cf   |  2 ++
 restic/README.md     |  9 +++++----
 9 files changed, 61 insertions(+), 11 deletions(-)
 create mode 100644 postfix/alias.cf
 create mode 100644 postfix/domains.cf
 create mode 100644 postfix/main.cf
 create mode 100644 postfix/relay.cf
 create mode 100644 postfix/schema.sql
 create mode 100644 postfix/transport.cf
 create mode 100644 postfix/virtual.cf

diff --git a/nextcloud/README.md b/nextcloud/README.md
index bdb6bc9..6d2886e 100644
--- a/nextcloud/README.md
+++ b/nextcloud/README.md
@@ -10,16 +10,11 @@ sudo mkdir -p /data/cloud
 sudo chown cloud.cloud /data/cloud
 ```
 
-## Variables
-
-```sh
-PHPVERSION=x.x
-DOMAIN=example.com
-```
-
 ## Install software
 
 ```sh
+PHPVERSION=x.x
+
 sudo apt install redis-server nginx php-redis php$PHPVERSION-{cli,curl,fpm,gd,intl,mbstring,sqlite3,xml,zip}
 
 wget https://download.nextcloud.com/server/releases/latest.zip
@@ -30,6 +25,8 @@ sudo -u cloud unzip ~/latest.zip
 ## Copy configuration
 
 ```sh
+DOMAIN=example.com
+
 sudo cp nginx.conf /etc/nginx/sites-available/nextcloud
 sudo cp fpm.conf /etc/php/*/fpm/pool.d/nextcloud.conf
 cat redis.conf | sudo tee -a /etc/redis/redis.conf
@@ -38,4 +35,10 @@ sudo -u cloud cp nextcloud.conf.php /data/cloud/nextcloud/config/local.config.ph
 sudo -u cloud crontab crontab
 
 sudo sed -i s/example.com/$DOMAIN/g /etc/nginx/sites-available/nextcloud /data/cloud/nextcloud/config/local.config.php
+
+cd /etc/nginx/sites-enabled
+sudo ln -s ../sites-available/nextcloud
+
+sudo -u cloud mkdir -p /data/cloud/.config/user-tmpfiles.d
+echo 'e %h/data/*/files/tmp - - - 7d' | sudo -u cloud tee /data/cloud/.config/user-tmpfiles.d/nextcloud-tmp.conf
 ```
diff --git a/postfix/alias.cf b/postfix/alias.cf
new file mode 100644
index 0000000..f061aaa
--- /dev/null
+++ b/postfix/alias.cf
@@ -0,0 +1,2 @@
+dbpath = /etc/postfix/postfix.db
+query = SELECT goto FROM alias WHERE address = '%s' AND active = 1
diff --git a/postfix/domains.cf b/postfix/domains.cf
new file mode 100644
index 0000000..d55aa30
--- /dev/null
+++ b/postfix/domains.cf
@@ -0,0 +1,2 @@
+dbpath = /etc/postfix/postfix.db
+query = SELECT domain FROM domain WHERE domain = '%s' AND active = 1 AND NOT (transport LIKE 'smtp%%' OR transport LIKE 'relay%%')
diff --git a/postfix/main.cf b/postfix/main.cf
new file mode 100644
index 0000000..42cb269
--- /dev/null
+++ b/postfix/main.cf
@@ -0,0 +1,33 @@
+# System
+
+biff = no
+compatibility_level = 2
+disable_vrfy_command = yes
+mailbox_size_limit = 0
+message_size_limit = 0
+mydomain = local
+mynetworks_style = subnet
+
+# TLS
+
+smtp_tls_security_level = may
+smtpd_tls_security_level = may
+smtpd_tls_key_file =  /data/ssl/certs/mail.adrian.kousz.ch/privkey.pem
+smtpd_tls_cert_file = /data/ssl/certs/mail.adrian.kousz.ch/fullchain.pem
+
+# Custom
+
+relay_domains = sqlite:/etc/postfix/relay.cf
+transport_maps = sqlite:/etc/postfix/transport.cf
+
+recipient_delimiter = +
+virtual_mailbox_base = /data/mail/box
+virtual_uid_maps = static:2000
+virtual_gid_maps = static:2000
+virtual_mailbox_domains = sqlite:/etc/postfix/domains.cf
+virtual_mailbox_maps = sqlite:/etc/postfix/virtual.cf
+virtual_alias_maps = sqlite:/etc/postfix/alias.cf
+virtual_mailbox_limit = 0
+
+smtpd_milters = unix:/run/opendkim/opendkim.socket
+non_smtpd_milters = $smtpd_milters
diff --git a/postfix/relay.cf b/postfix/relay.cf
new file mode 100644
index 0000000..674ff7b
--- /dev/null
+++ b/postfix/relay.cf
@@ -0,0 +1,2 @@
+dbpath = /etc/postfix/postfix.db
+query = SELECT domain FROM domain WHERE domain = '%s' AND active = 1 AND (transport LIKE 'smtp%%' OR transport LIKE 'relay%%')
diff --git a/postfix/schema.sql b/postfix/schema.sql
new file mode 100644
index 0000000..f123723
--- /dev/null
+++ b/postfix/schema.sql
@@ -0,0 +1,3 @@
+create table alias (address varchar(255) not null primary key, goto varchar(255) not null, active int not null default 1);
+create table domain (domain varchar(255) not null primary key, transport varchar(255) not null default 'virtual', active int not null default 1);
+create table mailbox (username varchar(255) not null primary key, password varchar(255) not null default '', name varchar(255) not null default '', maildir varchar(255) not null, active int not null default 1);
diff --git a/postfix/transport.cf b/postfix/transport.cf
new file mode 100644
index 0000000..c4acd99
--- /dev/null
+++ b/postfix/transport.cf
@@ -0,0 +1,2 @@
+dbpath = /etc/postfix/postfix.db
+query = SELECT transport FROM domain WHERE domain = '%s' AND active = 1
diff --git a/postfix/virtual.cf b/postfix/virtual.cf
new file mode 100644
index 0000000..3372c2d
--- /dev/null
+++ b/postfix/virtual.cf
@@ -0,0 +1,2 @@
+dbpath = /etc/postfix/postfix.db
+query = SELECT maildir FROM mailbox WHERE username = '%s' AND active = 1
diff --git a/restic/README.md b/restic/README.md
index 2590eed..93d82a2 100644
--- a/restic/README.md
+++ b/restic/README.md
@@ -4,17 +4,18 @@ Download binary:
 https://github.com/restic/restic/releases/latest
 
 ```sh
+REPO=sftp:backup-user@example.com:repo
+
 bunzip2 restic*.bz2
 sudo cp restic* /usr/local/bin/restic
 
-echo 'nice /usr/local/bin/restic -r REPO -p /usr/local/share/key "$@"' | sudo tee /usr/local/bin/restic-cmd
+echo 'nice /usr/local/bin/restic -r' "$REPO" '-p /root/backup-key "$@"' | sudo tee /usr/local/bin/restic-cmd
 sudo chmod +x /usr/local/bin/restic-cmd
 sudo nano /usr/local/bin/restic-cmd
 
-cat /dev/urandom | base64 | head -c 64 | sudo tee /usr/local/share/key
-sudo chmod 0600 /usr/local/share/key
+cat /dev/urandom | base64 | head -c 64 | sudo tee /root/backup-key
+sudo chmod 0600 /root/backup-key
 
 sudo restic-cmd init
 sudo crontab crontab
 ```
-

From 1b3bc0f9a84ac672fb3bee01dae343c3e5abf474 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sun, 16 Feb 2020 01:04:08 +0100
Subject: [PATCH 04/37] Update

---
 mail/README.md                         | 16 ++++++++++++++++
 {postfix => mail/postfix}/alias.cf     |  2 +-
 {postfix => mail/postfix}/domains.cf   |  2 +-
 {postfix => mail/postfix}/main.cf      |  0
 {postfix => mail/postfix}/relay.cf     |  2 +-
 {postfix => mail/postfix}/transport.cf |  2 +-
 {postfix => mail/postfix}/virtual.cf   |  2 +-
 {postfix => mail}/schema.sql           |  0
 nginx/README.md                        |  2 +-
 ssh/README.md                          |  2 +-
 10 files changed, 23 insertions(+), 7 deletions(-)
 create mode 100644 mail/README.md
 rename {postfix => mail/postfix}/alias.cf (67%)
 rename {postfix => mail/postfix}/domains.cf (79%)
 rename {postfix => mail/postfix}/main.cf (100%)
 rename {postfix => mail/postfix}/relay.cf (79%)
 rename {postfix => mail/postfix}/transport.cf (68%)
 rename {postfix => mail/postfix}/virtual.cf (68%)
 rename {postfix => mail}/schema.sql (100%)

diff --git a/mail/README.md b/mail/README.md
new file mode 100644
index 0000000..3e00371
--- /dev/null
+++ b/mail/README.md
@@ -0,0 +1,16 @@
+# Postfix with SQLite
+
+```sh
+echo 'vmail:*:2000:2000::/data/mail:/usr/sbin/nologin' | sudo tee -a /etc/passwd
+echo 'vmail:x:2000:' | sudo tee -a /etc/group
+
+sudo mkdir -p /data/mail
+sudo chown vmail.vmail /data/mail
+
+sudo apt install postfix postfix-sqlite
+sudo cp postfix/*.cf /etc/postfix
+
+cat schema.sql | sudo sqlite3 /data/mail/postfix.db
+sudo chown vmail.postfix /data/mail/postfix.db
+sudo chmod 640 /data/mail/postfix.db
+```
diff --git a/postfix/alias.cf b/mail/postfix/alias.cf
similarity index 67%
rename from postfix/alias.cf
rename to mail/postfix/alias.cf
index f061aaa..35c26ef 100644
--- a/postfix/alias.cf
+++ b/mail/postfix/alias.cf
@@ -1,2 +1,2 @@
-dbpath = /etc/postfix/postfix.db
+dbpath = /data/mail/postfix.db
 query = SELECT goto FROM alias WHERE address = '%s' AND active = 1
diff --git a/postfix/domains.cf b/mail/postfix/domains.cf
similarity index 79%
rename from postfix/domains.cf
rename to mail/postfix/domains.cf
index d55aa30..9d2e909 100644
--- a/postfix/domains.cf
+++ b/mail/postfix/domains.cf
@@ -1,2 +1,2 @@
-dbpath = /etc/postfix/postfix.db
+dbpath = /data/mail/postfix.db
 query = SELECT domain FROM domain WHERE domain = '%s' AND active = 1 AND NOT (transport LIKE 'smtp%%' OR transport LIKE 'relay%%')
diff --git a/postfix/main.cf b/mail/postfix/main.cf
similarity index 100%
rename from postfix/main.cf
rename to mail/postfix/main.cf
diff --git a/postfix/relay.cf b/mail/postfix/relay.cf
similarity index 79%
rename from postfix/relay.cf
rename to mail/postfix/relay.cf
index 674ff7b..9d381ab 100644
--- a/postfix/relay.cf
+++ b/mail/postfix/relay.cf
@@ -1,2 +1,2 @@
-dbpath = /etc/postfix/postfix.db
+dbpath = /data/mail/postfix.db
 query = SELECT domain FROM domain WHERE domain = '%s' AND active = 1 AND (transport LIKE 'smtp%%' OR transport LIKE 'relay%%')
diff --git a/postfix/transport.cf b/mail/postfix/transport.cf
similarity index 68%
rename from postfix/transport.cf
rename to mail/postfix/transport.cf
index c4acd99..e57dd01 100644
--- a/postfix/transport.cf
+++ b/mail/postfix/transport.cf
@@ -1,2 +1,2 @@
-dbpath = /etc/postfix/postfix.db
+dbpath = /data/mail/postfix.db
 query = SELECT transport FROM domain WHERE domain = '%s' AND active = 1
diff --git a/postfix/virtual.cf b/mail/postfix/virtual.cf
similarity index 68%
rename from postfix/virtual.cf
rename to mail/postfix/virtual.cf
index 3372c2d..661e009 100644
--- a/postfix/virtual.cf
+++ b/mail/postfix/virtual.cf
@@ -1,2 +1,2 @@
-dbpath = /etc/postfix/postfix.db
+dbpath = /data/mail/postfix.db
 query = SELECT maildir FROM mailbox WHERE username = '%s' AND active = 1
diff --git a/postfix/schema.sql b/mail/schema.sql
similarity index 100%
rename from postfix/schema.sql
rename to mail/schema.sql
diff --git a/nginx/README.md b/nginx/README.md
index 07cce4f..8d37854 100644
--- a/nginx/README.md
+++ b/nginx/README.md
@@ -3,7 +3,7 @@
 ## Copy configuration
 
 ```sh
-sudo cp -r s* /etc/nginx
+sudo cp -r sites-available snippets /etc/nginx
 
 cd /etc/nginx/sites-enabled
 sudo ln -s ../sites-available/0nohost
diff --git a/ssh/README.md b/ssh/README.md
index 052e596..98a8fa6 100644
--- a/ssh/README.md
+++ b/ssh/README.md
@@ -4,7 +4,7 @@ Use only one user `sshlogin` for logins to the server.
 Switch to your main user with `su - adminuser` afterwards.
 
 ```sh
-echo 'sshlogin:x:999:65534::/home/sshlogin:/bin/sh' | sudo tee -a /etc/passwd
+echo 'sshlogin:x:1001:65534::/home/sshlogin:/bin/sh' | sudo tee -a /etc/passwd
 cat sshd_config | sudo tee -a /etc/ssh/sshd_config
 
 sudo mkdir -p /home/sshlogin/.ssh

From fa12798ae96bad7f33bfcdce6a298623ae859f21 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sun, 16 Feb 2020 16:54:16 +0100
Subject: [PATCH 05/37] Update mail

---
 mail/README.md                  | 62 ++++++++++++++++++++++++++++-----
 mail/dovecot/local-sql.conf.ext |  9 +++++
 mail/dovecot/local.conf         | 17 +++++++++
 mail/opendkim/local.conf        | 11 ++++++
 mail/postfix/alias.cf           |  2 +-
 mail/postfix/domains.cf         |  2 +-
 mail/postfix/main.cf            | 10 +++---
 mail/postfix/relay.cf           |  2 +-
 mail/postfix/transport.cf       |  2 +-
 mail/postfix/virtual.cf         |  2 +-
 mail/schema.sql                 |  3 ++
 11 files changed, 103 insertions(+), 19 deletions(-)
 create mode 100644 mail/dovecot/local-sql.conf.ext
 create mode 100644 mail/dovecot/local.conf
 create mode 100644 mail/opendkim/local.conf

diff --git a/mail/README.md b/mail/README.md
index 3e00371..5e757fe 100644
--- a/mail/README.md
+++ b/mail/README.md
@@ -1,16 +1,60 @@
-# Postfix with SQLite
+# Mail with SQLite
+
+## Create User
 
 ```sh
 echo 'vmail:*:2000:2000::/data/mail:/usr/sbin/nologin' | sudo tee -a /etc/passwd
 echo 'vmail:x:2000:' | sudo tee -a /etc/group
 
-sudo mkdir -p /data/mail
-sudo chown vmail.vmail /data/mail
+sudo mkdir -p /data/mail/mail
+sudo mkdir -p /data/mail/config
+sudo chown vmail: /data/mail/*
 
-sudo apt install postfix postfix-sqlite
-sudo cp postfix/*.cf /etc/postfix
-
-cat schema.sql | sudo sqlite3 /data/mail/postfix.db
-sudo chown vmail.postfix /data/mail/postfix.db
-sudo chmod 640 /data/mail/postfix.db
+cat schema.sql | sudo -u vmail sqlite3 /data/mail/config/vmail.db
+sudo chown vmail:postfix /data/mail/config/vmail.db
+sudo chmod 640 /data/mail/config/vmail.db
 ```
+
+## Install Software
+
+```sh
+sudo apt install sqlite3 postfix postfix-sqlite dovecot-imapd dovecot-sqlite opendkim
+```
+
+### Create opendkim config directory in Debian
+
+```sh
+echo 'Include /etc/opendkim/local.conf' | sudo tee -a /etc/opendkim.conf
+sudo mkdir /etc/opendkim
+sudo mv /etc/opendkim.conf /etc/opendkim
+echo 'Include /etc/opendkim/opendkim.conf' | sudo tee /etc/opendkim.conf
+```
+
+### Prepare for opendkim socket operation
+
+```sh
+sudo mkdir /var/spool/postfix/opendkim
+sudo chown opendkim: /var/spool/postfix/opendkim
+sudo chmod 750 /var/spool/postfix/opendkim
+sudo adduser postfix opendkim
+```
+
+## Apply Configuration
+
+```sh
+DOMAIN=example.com
+
+sudo ln -s /data/mail/config/vmail.db /.opendkim-bug-241.db
+sudo opendkim-genkey -D /data/mail -d $DOMAIN -s s
+
+sudo cp -r postfix dovecot opendkim /etc
+sudo sed -i s/example.com/$DOMAIN/g /etc/postfix/main.cf /etc/dovecot/local.conf
+```
+
+## Notes
+
+* The vmail database parent directory needs to be writeable by the user modifying the database
+* The postfix process does not load the supplementary groups (`set_eugid` only sets one gid),
+  hence the vmail database needs to be readable by the postfix primary group
+* The dovecot process runs as root and can access the database
+* OpenDKIM's `dsn` parsing is broken and opens the database in the root directory
diff --git a/mail/dovecot/local-sql.conf.ext b/mail/dovecot/local-sql.conf.ext
new file mode 100644
index 0000000..ee22dec
--- /dev/null
+++ b/mail/dovecot/local-sql.conf.ext
@@ -0,0 +1,9 @@
+driver = sqlite
+connect = /data/mail/config/vmail.db
+
+user_query = SELECT \
+ REPLACE('/data/mail/mail/{dir}', '{dir}', maildir) AS home \
+ , REPLACE('maildir:/data/mail/mail/{dir}:LAYOUT=fs', '{dir}', maildir) AS mail \
+ FROM mailbox WHERE username = '%u' AND active = 1
+
+password_query = SELECT password FROM mailbox WHERE username = '%u'
diff --git a/mail/dovecot/local.conf b/mail/dovecot/local.conf
new file mode 100644
index 0000000..ce2c049
--- /dev/null
+++ b/mail/dovecot/local.conf
@@ -0,0 +1,17 @@
+protocols = imap
+mail_uid = vmail
+mail_gid = vmail
+
+ssl = required
+ssl_key = </data/ssl/certs/mail.example.com/privkey.pem
+ssl_cert = </data/ssl/certs/mail.example.com/fullchain.pem
+
+userdb {
+	driver = sql
+	args = /etc/dovecot/local-sql.conf.ext
+}
+
+passdb {
+	driver = sql
+	args = /etc/dovecot/local-sql.conf.ext
+}
diff --git a/mail/opendkim/local.conf b/mail/opendkim/local.conf
new file mode 100644
index 0000000..6b72716
--- /dev/null
+++ b/mail/opendkim/local.conf
@@ -0,0 +1,11 @@
+Socket local:/var/spool/postfix/opendkim/opendkim.sock
+InternalHosts 0.0.0.0/0
+
+Canonicalization relaxed/relaxed
+
+KeyTable dsn:sqlite://./opendkim-bug-241.db/table=dkim_key?keycol=key?datacol=domain,selector,private_key
+SigningTable dsn:sqlite://./opendkim-bug-241.db/table=dkim?keycol=match?datacol=key
+
+# Domain example.com
+# Selector s
+# KeyFile /data/mail/s.private
diff --git a/mail/postfix/alias.cf b/mail/postfix/alias.cf
index 35c26ef..856c3cc 100644
--- a/mail/postfix/alias.cf
+++ b/mail/postfix/alias.cf
@@ -1,2 +1,2 @@
-dbpath = /data/mail/postfix.db
+dbpath = /data/mail/config/vmail.db
 query = SELECT goto FROM alias WHERE address = '%s' AND active = 1
diff --git a/mail/postfix/domains.cf b/mail/postfix/domains.cf
index 9d2e909..619b14a 100644
--- a/mail/postfix/domains.cf
+++ b/mail/postfix/domains.cf
@@ -1,2 +1,2 @@
-dbpath = /data/mail/postfix.db
+dbpath = /data/mail/config/vmail.db
 query = SELECT domain FROM domain WHERE domain = '%s' AND active = 1 AND NOT (transport LIKE 'smtp%%' OR transport LIKE 'relay%%')
diff --git a/mail/postfix/main.cf b/mail/postfix/main.cf
index 42cb269..849d610 100644
--- a/mail/postfix/main.cf
+++ b/mail/postfix/main.cf
@@ -5,15 +5,15 @@ compatibility_level = 2
 disable_vrfy_command = yes
 mailbox_size_limit = 0
 message_size_limit = 0
-mydomain = local
+mydomain = example.com
 mynetworks_style = subnet
 
 # TLS
 
 smtp_tls_security_level = may
 smtpd_tls_security_level = may
-smtpd_tls_key_file =  /data/ssl/certs/mail.adrian.kousz.ch/privkey.pem
-smtpd_tls_cert_file = /data/ssl/certs/mail.adrian.kousz.ch/fullchain.pem
+smtpd_tls_key_file =  /data/ssl/certs/mail.example.com/privkey.pem
+smtpd_tls_cert_file = /data/ssl/certs/mail.example.com/fullchain.pem
 
 # Custom
 
@@ -21,7 +21,7 @@ relay_domains = sqlite:/etc/postfix/relay.cf
 transport_maps = sqlite:/etc/postfix/transport.cf
 
 recipient_delimiter = +
-virtual_mailbox_base = /data/mail/box
+virtual_mailbox_base = /data/mail/mail
 virtual_uid_maps = static:2000
 virtual_gid_maps = static:2000
 virtual_mailbox_domains = sqlite:/etc/postfix/domains.cf
@@ -29,5 +29,5 @@ virtual_mailbox_maps = sqlite:/etc/postfix/virtual.cf
 virtual_alias_maps = sqlite:/etc/postfix/alias.cf
 virtual_mailbox_limit = 0
 
-smtpd_milters = unix:/run/opendkim/opendkim.socket
+smtpd_milters = unix:/opendkim/opendkim.socket
 non_smtpd_milters = $smtpd_milters
diff --git a/mail/postfix/relay.cf b/mail/postfix/relay.cf
index 9d381ab..7fe86eb 100644
--- a/mail/postfix/relay.cf
+++ b/mail/postfix/relay.cf
@@ -1,2 +1,2 @@
-dbpath = /data/mail/postfix.db
+dbpath = /data/mail/config/vmail.db
 query = SELECT domain FROM domain WHERE domain = '%s' AND active = 1 AND (transport LIKE 'smtp%%' OR transport LIKE 'relay%%')
diff --git a/mail/postfix/transport.cf b/mail/postfix/transport.cf
index e57dd01..0e8deac 100644
--- a/mail/postfix/transport.cf
+++ b/mail/postfix/transport.cf
@@ -1,2 +1,2 @@
-dbpath = /data/mail/postfix.db
+dbpath = /data/mail/config/vmail.db
 query = SELECT transport FROM domain WHERE domain = '%s' AND active = 1
diff --git a/mail/postfix/virtual.cf b/mail/postfix/virtual.cf
index 661e009..dd3705d 100644
--- a/mail/postfix/virtual.cf
+++ b/mail/postfix/virtual.cf
@@ -1,2 +1,2 @@
-dbpath = /data/mail/postfix.db
+dbpath = /data/mail/config/vmail.db
 query = SELECT maildir FROM mailbox WHERE username = '%s' AND active = 1
diff --git a/mail/schema.sql b/mail/schema.sql
index f123723..7bc9222 100644
--- a/mail/schema.sql
+++ b/mail/schema.sql
@@ -1,3 +1,6 @@
 create table alias (address varchar(255) not null primary key, goto varchar(255) not null, active int not null default 1);
 create table domain (domain varchar(255) not null primary key, transport varchar(255) not null default 'virtual', active int not null default 1);
 create table mailbox (username varchar(255) not null primary key, password varchar(255) not null default '', name varchar(255) not null default '', maildir varchar(255) not null, active int not null default 1);
+
+create table dkim (match varchar(255) not null primary key, key varchar(255) not null);
+create table dkim_key (key varchar(255) not null primary key, domain varchar(255) not null, selector varchar(255) not null, private_key varchar(65535) not null);

From 78aa0d023f89fb5b42408253b2592ab69d75fa93 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sun, 16 Feb 2020 16:54:55 +0100
Subject: [PATCH 06/37] Update

---
 nextcloud/README.md   | 8 ++++----
 nginx/README.md       | 4 +---
 nginx/conf.d/ssl.conf | 2 ++
 restic/crontab        | 2 +-
 ssh/README.md         | 2 +-
 5 files changed, 9 insertions(+), 9 deletions(-)
 create mode 100644 nginx/conf.d/ssl.conf

diff --git a/nextcloud/README.md b/nextcloud/README.md
index 6d2886e..bccb2f5 100644
--- a/nextcloud/README.md
+++ b/nextcloud/README.md
@@ -1,16 +1,16 @@
 # Nextcloud
 
-## Create user
+## Create User
 
 ```sh
 echo 'cloud:*:3000:3000::/data/cloud:/bin/sh' | sudo tee -a /etc/passwd
 echo 'cloud:x:3000:' | sudo tee -a /etc/group
 
 sudo mkdir -p /data/cloud
-sudo chown cloud.cloud /data/cloud
+sudo chown cloud: /data/cloud
 ```
 
-## Install software
+## Install Software
 
 ```sh
 PHPVERSION=x.x
@@ -22,7 +22,7 @@ cd /data/cloud
 sudo -u cloud unzip ~/latest.zip
 ```
 
-## Copy configuration
+## Apply Configuration
 
 ```sh
 DOMAIN=example.com
diff --git a/nginx/README.md b/nginx/README.md
index 8d37854..09c9170 100644
--- a/nginx/README.md
+++ b/nginx/README.md
@@ -1,9 +1,7 @@
 # Nginx
 
-## Copy configuration
-
 ```sh
-sudo cp -r sites-available snippets /etc/nginx
+sudo cp -r sites-available snippets conf.d /etc/nginx
 
 cd /etc/nginx/sites-enabled
 sudo ln -s ../sites-available/0nohost
diff --git a/nginx/conf.d/ssl.conf b/nginx/conf.d/ssl.conf
new file mode 100644
index 0000000..9171037
--- /dev/null
+++ b/nginx/conf.d/ssl.conf
@@ -0,0 +1,2 @@
+ssl_certificate /path/to/fullchain.pem;
+ssl_certificate_key /path/to/privkey.pem;
diff --git a/restic/crontab b/restic/crontab
index cb74a6b..69bc4f2 100644
--- a/restic/crontab
+++ b/restic/crontab
@@ -1,2 +1,2 @@
 48 *	* * *	/usr/local/bin/restic-cmd backup -q --exclude-if-present .nobackup /data
-18 3	* * *	/usr/local/bin/restic-cmd forget -q --keep-tag keep -H 24 -d 7 -m 24 -y 100
+18 3	* * *	/usr/local/bin/restic-cmd forget -q --keep-tag keep -H 24 -d 7 -m 12 -y 100
diff --git a/ssh/README.md b/ssh/README.md
index 98a8fa6..3fdd920 100644
--- a/ssh/README.md
+++ b/ssh/README.md
@@ -8,7 +8,7 @@ echo 'sshlogin:x:1001:65534::/home/sshlogin:/bin/sh' | sudo tee -a /etc/passwd
 cat sshd_config | sudo tee -a /etc/ssh/sshd_config
 
 sudo mkdir -p /home/sshlogin/.ssh
-sudo chown sshlogin.sshlogin /home/sshlogin/.ssh
+sudo chown sshlogin:root /home/sshlogin/.ssh
 ```
 
 * Either create a password with `sudo passwd sshlogin` or

From 8d55ae3513d6f8168c7ceb1b81bff387143462cf Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sun, 16 Feb 2020 18:48:12 +0100
Subject: [PATCH 07/37] Inbucket

---
 inbucket/README.md        | 35 +++++++++++++++++++++++++++++++++++
 inbucket/inbucket.conf    |  9 +++++++++
 inbucket/inbucket.service | 11 +++++++++++
 inbucket/nginx.conf       | 20 ++++++++++++++++++++
 4 files changed, 75 insertions(+)
 create mode 100644 inbucket/README.md
 create mode 100644 inbucket/inbucket.conf
 create mode 100644 inbucket/inbucket.service
 create mode 100644 inbucket/nginx.conf

diff --git a/inbucket/README.md b/inbucket/README.md
new file mode 100644
index 0000000..2807b25
--- /dev/null
+++ b/inbucket/README.md
@@ -0,0 +1,35 @@
+# Inbucket
+
+To be used together with the [`mail`](../mail) setup.
+
+## Install Software
+
+Get: https://www.inbucket.org/binaries/
+
+```sh
+cd /usr/local/share
+sudo tar -xf ~/inbucket_*
+sudo ln -s inbucket_* inbucket
+sudo ln -s ../share/inbucket/inbucket /usr/local/bin
+sudo ln -s ../share/inbucket/inbucket-client /usr/local/bin
+```
+
+## Apply Configuration
+
+```sh
+DOMAIN=test.local
+
+sudo cp inbucket.service /etc/systemd/system
+sudo -u vmail cp inbucket.conf /data/mail/config
+sudo cp nginx.conf /etc/nginx/sites-available/inbucket
+
+sudo ln -s ../sites-available/inbucket /etc/nginx/sites-enabled
+
+echo "inbucket:$(openssl passwd -5)" | sudo -u vmail tee /data/mail/config/inbucket.htpasswd
+sudo chown vmail:www-data /data/mail/config/inbucket.htpasswd
+sudo chmod 640 /data/mail/config/inbucket.htpasswd
+
+echo "insert into domain (domain, transport) values ('$DOMAIN','smtp:[localhost]:2500')" | sudo -u vmail sqlite3 /data/mail/config/vmail.db
+
+sudo systemctl enable inbucket
+```
diff --git a/inbucket/inbucket.conf b/inbucket/inbucket.conf
new file mode 100644
index 0000000..8a1817a
--- /dev/null
+++ b/inbucket/inbucket.conf
@@ -0,0 +1,9 @@
+INBUCKET_LOGLEVEL=warn
+INBUCKET_MAILBOXNAMING=full
+INBUCKET_WEB_ADDR=0.0.0.0:8025
+INBUCKET_WEB_UIDIR=/usr/local/share/inbucket/ui
+INBUCKET_WEB_GREETINGFILE=/usr/local/share/inbucket/ui/greeting.html
+INBUCKET_STORAGE_TYPE=file
+INBUCKET_STORAGE_PARAMS=path:/data/mail
+INBUCKET_STORAGE_RETENTIONPERIOD=0
+INBUCKET_STORAGE_MAILBOXMSGCAP=0
diff --git a/inbucket/inbucket.service b/inbucket/inbucket.service
new file mode 100644
index 0000000..5f1d255
--- /dev/null
+++ b/inbucket/inbucket.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Inbucket
+After=network.target
+
+[Service]
+ExecStart=/usr/local/bin/inbucket
+EnvironmentFile=/data/mail/config/inbucket.conf
+User=vmail
+
+[Install]
+WantedBy=multi-user.target
diff --git a/inbucket/nginx.conf b/inbucket/nginx.conf
new file mode 100644
index 0000000..ff52ffd
--- /dev/null
+++ b/inbucket/nginx.conf
@@ -0,0 +1,20 @@
+server {
+	server_name mail.example.com;
+
+	listen 443 ssl;
+
+	auth_basic "Mail";
+	auth_basic_user_file /data/mail/config/inbucket.htpasswd;
+
+	location / {
+		proxy_pass http://localhost:8025;
+		include proxy_params;
+	}
+
+	location /api {
+		proxy_pass http://localhost:8025;
+		include proxy_params;
+		proxy_set_header Upgrade $http_upgrade;
+		proxy_set_header Connection "Upgrade";
+	}
+}

From 59219ec6444c3faead731999bc57390dcbdba6b1 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sun, 16 Feb 2020 18:48:16 +0100
Subject: [PATCH 08/37] Update

---
 nextcloud/README.md                                | 8 +++-----
 nextcloud/{nextcloud.conf.php => local.config.php} | 0
 2 files changed, 3 insertions(+), 5 deletions(-)
 rename nextcloud/{nextcloud.conf.php => local.config.php} (100%)

diff --git a/nextcloud/README.md b/nextcloud/README.md
index bccb2f5..596ba62 100644
--- a/nextcloud/README.md
+++ b/nextcloud/README.md
@@ -18,8 +18,7 @@ PHPVERSION=x.x
 sudo apt install redis-server nginx php-redis php$PHPVERSION-{cli,curl,fpm,gd,intl,mbstring,sqlite3,xml,zip}
 
 wget https://download.nextcloud.com/server/releases/latest.zip
-cd /data/cloud
-sudo -u cloud unzip ~/latest.zip
+sudo -u cloud unzip -d /data/cloud ~/latest.zip
 ```
 
 ## Apply Configuration
@@ -31,13 +30,12 @@ sudo cp nginx.conf /etc/nginx/sites-available/nextcloud
 sudo cp fpm.conf /etc/php/*/fpm/pool.d/nextcloud.conf
 cat redis.conf | sudo tee -a /etc/redis/redis.conf
 
-sudo -u cloud cp nextcloud.conf.php /data/cloud/nextcloud/config/local.config.php
+sudo -u cloud cp local.config.php /data/cloud/nextcloud/config
 sudo -u cloud crontab crontab
 
 sudo sed -i s/example.com/$DOMAIN/g /etc/nginx/sites-available/nextcloud /data/cloud/nextcloud/config/local.config.php
 
-cd /etc/nginx/sites-enabled
-sudo ln -s ../sites-available/nextcloud
+sudo ln -s ../sites-available/nextcloud /etc/nginx/sites-enabled
 
 sudo -u cloud mkdir -p /data/cloud/.config/user-tmpfiles.d
 echo 'e %h/data/*/files/tmp - - - 7d' | sudo -u cloud tee /data/cloud/.config/user-tmpfiles.d/nextcloud-tmp.conf
diff --git a/nextcloud/nextcloud.conf.php b/nextcloud/local.config.php
similarity index 100%
rename from nextcloud/nextcloud.conf.php
rename to nextcloud/local.config.php

From 91fba3cbd76e7d718fe7c9d08592ad12f5a7b596 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sun, 16 Feb 2020 21:55:11 +0100
Subject: [PATCH 09/37] Update

---
 mail/README.md              | 27 +++++----------------------
 mail/dovecot/local.conf     |  6 ++++++
 mail/opendkim/local.conf    |  3 ++-
 mail/postfix/login.cf       |  2 ++
 mail/postfix/main.cf        |  8 +++++++-
 named/README.md             | 19 +++++++++++++++++++
 named/badlist-active.zone   |  8 ++++++++
 named/badlist-inactive.zone |  3 +++
 named/options.conf          |  3 +++
 named/zones.conf            |  1 +
 nextcloud/README.md         |  6 +++---
 restic/README.md            |  1 -
 ssh/README.md               |  4 ++--
 13 files changed, 61 insertions(+), 30 deletions(-)
 create mode 100644 mail/postfix/login.cf
 create mode 100644 named/README.md
 create mode 100644 named/badlist-active.zone
 create mode 100644 named/badlist-inactive.zone
 create mode 100644 named/options.conf
 create mode 100644 named/zones.conf

diff --git a/mail/README.md b/mail/README.md
index 5e757fe..a7b1410 100644
--- a/mail/README.md
+++ b/mail/README.md
@@ -3,8 +3,8 @@
 ## Create User
 
 ```sh
-echo 'vmail:*:2000:2000::/data/mail:/usr/sbin/nologin' | sudo tee -a /etc/passwd
-echo 'vmail:x:2000:' | sudo tee -a /etc/group
+sudo sed -i '$ a vmail:*:2000:2000::/data/mail:/usr/sbin/nologin' /etc/passwd
+sudo sed -i '$ a vmail:x:2000:' /etc/passwd
 
 sudo mkdir -p /data/mail/mail
 sudo mkdir -p /data/mail/config
@@ -21,24 +21,6 @@ sudo chmod 640 /data/mail/config/vmail.db
 sudo apt install sqlite3 postfix postfix-sqlite dovecot-imapd dovecot-sqlite opendkim
 ```
 
-### Create opendkim config directory in Debian
-
-```sh
-echo 'Include /etc/opendkim/local.conf' | sudo tee -a /etc/opendkim.conf
-sudo mkdir /etc/opendkim
-sudo mv /etc/opendkim.conf /etc/opendkim
-echo 'Include /etc/opendkim/opendkim.conf' | sudo tee /etc/opendkim.conf
-```
-
-### Prepare for opendkim socket operation
-
-```sh
-sudo mkdir /var/spool/postfix/opendkim
-sudo chown opendkim: /var/spool/postfix/opendkim
-sudo chmod 750 /var/spool/postfix/opendkim
-sudo adduser postfix opendkim
-```
-
 ## Apply Configuration
 
 ```sh
@@ -47,13 +29,14 @@ DOMAIN=example.com
 sudo ln -s /data/mail/config/vmail.db /.opendkim-bug-241.db
 sudo opendkim-genkey -D /data/mail -d $DOMAIN -s s
 
-sudo cp -r postfix dovecot opendkim /etc
+sudo cp -r postfix dovecot /etc
+sudo sed -i '$ r opendkim/local.conf' /etc/opendkim.conf
 sudo sed -i s/example.com/$DOMAIN/g /etc/postfix/main.cf /etc/dovecot/local.conf
 ```
 
 ## Notes
 
-* The vmail database parent directory needs to be writeable by the user modifying the database
+* The `vmail.db` parent directory needs to be writeable by the user modifying the database
 * The postfix process does not load the supplementary groups (`set_eugid` only sets one gid),
   hence the vmail database needs to be readable by the postfix primary group
 * The dovecot process runs as root and can access the database
diff --git a/mail/dovecot/local.conf b/mail/dovecot/local.conf
index ce2c049..fd9ce3e 100644
--- a/mail/dovecot/local.conf
+++ b/mail/dovecot/local.conf
@@ -15,3 +15,9 @@ passdb {
 	driver = sql
 	args = /etc/dovecot/local-sql.conf.ext
 }
+
+service auth {
+	unix_listener /var/spool/postfix/private/auth {
+		user = postfix
+	}
+}
diff --git a/mail/opendkim/local.conf b/mail/opendkim/local.conf
index 6b72716..ebfd7ed 100644
--- a/mail/opendkim/local.conf
+++ b/mail/opendkim/local.conf
@@ -1,4 +1,5 @@
-Socket local:/var/spool/postfix/opendkim/opendkim.sock
+Socket local:/var/spool/postfix/private/opendkim
+UserID postfix
 InternalHosts 0.0.0.0/0
 
 Canonicalization relaxed/relaxed
diff --git a/mail/postfix/login.cf b/mail/postfix/login.cf
new file mode 100644
index 0000000..30acf06
--- /dev/null
+++ b/mail/postfix/login.cf
@@ -0,0 +1,2 @@
+dbpath = /data/mail/config/vmail.db
+query = SELECT username FROM mailbox WHERE username = '%s' AND active = 1
diff --git a/mail/postfix/main.cf b/mail/postfix/main.cf
index 849d610..8b60399 100644
--- a/mail/postfix/main.cf
+++ b/mail/postfix/main.cf
@@ -29,5 +29,11 @@ virtual_mailbox_maps = sqlite:/etc/postfix/virtual.cf
 virtual_alias_maps = sqlite:/etc/postfix/alias.cf
 virtual_mailbox_limit = 0
 
-smtpd_milters = unix:/opendkim/opendkim.socket
+smtpd_sasl_auth_enable = yes
+smtpd_sasl_type = dovecot
+smtpd_sasl_path = private/auth
+smtpd_sender_restrictions = reject_sender_login_mismatch
+smtpd_sender_login_maps = sqlite:/etc/postfix/login.cf, $virtual_alias_maps
+
+smtpd_milters = unix:private/opendkim
 non_smtpd_milters = $smtpd_milters
diff --git a/named/README.md b/named/README.md
new file mode 100644
index 0000000..520daa7
--- /dev/null
+++ b/named/README.md
@@ -0,0 +1,19 @@
+# Named
+
+## Install Software
+
+```sh
+sudo apt install bind9
+```
+
+## Apply Configuration
+
+```sh
+sudo mkdir /data/named
+sudo cp *.conf *.zone /data/named
+
+sudo sed -i '/directory/ a \\tinclude "/data/named/options.conf";' /etc/bind/named.conf.options
+sudo sed -i '$ a include "/data/named/zones.conf";' /etc/bind/named.conf
+
+echo '/data/named/** r,' | sudo tee -a /etc/apparmor.d/local/usr.sbin.named
+```
diff --git a/named/badlist-active.zone b/named/badlist-active.zone
new file mode 100644
index 0000000..ccd42f4
--- /dev/null
+++ b/named/badlist-active.zone
@@ -0,0 +1,8 @@
+$TTL 1H
+@	SOA	localhost. named-mgr.example.com (1 1W 1W 1W 1H)
+@	NS	localhost.
+
+facebook.com	CNAME	.
+*.facebook.com	CNAME	.
+instagram.com	CNAME	.
+*.instagram.com	CNAME	.
diff --git a/named/badlist-inactive.zone b/named/badlist-inactive.zone
new file mode 100644
index 0000000..e6bd4d3
--- /dev/null
+++ b/named/badlist-inactive.zone
@@ -0,0 +1,3 @@
+$TTL 1H
+@	SOA	localhost. named-mgr.example.com (1 1W 1W 1W 1H)
+@	NS	localhost.
diff --git a/named/options.conf b/named/options.conf
new file mode 100644
index 0000000..33723f3
--- /dev/null
+++ b/named/options.conf
@@ -0,0 +1,3 @@
+forward only;
+forwarders { 1.1.1.1; };
+response-policy { zone "badlist"; };
diff --git a/named/zones.conf b/named/zones.conf
new file mode 100644
index 0000000..b2cedf0
--- /dev/null
+++ b/named/zones.conf
@@ -0,0 +1 @@
+zone "badlist" { type master; file "/data/named/badlist-active.zone"; };
diff --git a/nextcloud/README.md b/nextcloud/README.md
index 596ba62..23e88ce 100644
--- a/nextcloud/README.md
+++ b/nextcloud/README.md
@@ -3,8 +3,8 @@
 ## Create User
 
 ```sh
-echo 'cloud:*:3000:3000::/data/cloud:/bin/sh' | sudo tee -a /etc/passwd
-echo 'cloud:x:3000:' | sudo tee -a /etc/group
+sudo sed -i '$ a cloud:*:3000:3000::/data/cloud:/bin/sh' /etc/passwd
+sudo sed -i '$ a cloud:x:3000:' /etc/group
 
 sudo mkdir -p /data/cloud
 sudo chown cloud: /data/cloud
@@ -28,7 +28,7 @@ DOMAIN=example.com
 
 sudo cp nginx.conf /etc/nginx/sites-available/nextcloud
 sudo cp fpm.conf /etc/php/*/fpm/pool.d/nextcloud.conf
-cat redis.conf | sudo tee -a /etc/redis/redis.conf
+sudo sed -i '$ r redis.conf' /etc/redis/redis.conf
 
 sudo -u cloud cp local.config.php /data/cloud/nextcloud/config
 sudo -u cloud crontab crontab
diff --git a/restic/README.md b/restic/README.md
index 93d82a2..3c23001 100644
--- a/restic/README.md
+++ b/restic/README.md
@@ -11,7 +11,6 @@ sudo cp restic* /usr/local/bin/restic
 
 echo 'nice /usr/local/bin/restic -r' "$REPO" '-p /root/backup-key "$@"' | sudo tee /usr/local/bin/restic-cmd
 sudo chmod +x /usr/local/bin/restic-cmd
-sudo nano /usr/local/bin/restic-cmd
 
 cat /dev/urandom | base64 | head -c 64 | sudo tee /root/backup-key
 sudo chmod 0600 /root/backup-key
diff --git a/ssh/README.md b/ssh/README.md
index 3fdd920..fdda9eb 100644
--- a/ssh/README.md
+++ b/ssh/README.md
@@ -4,8 +4,8 @@ Use only one user `sshlogin` for logins to the server.
 Switch to your main user with `su - adminuser` afterwards.
 
 ```sh
-echo 'sshlogin:x:1001:65534::/home/sshlogin:/bin/sh' | sudo tee -a /etc/passwd
-cat sshd_config | sudo tee -a /etc/ssh/sshd_config
+sudo sed -i '$ a sshlogin:x:1001:65534::/home/sshlogin:/bin/sh' /etc/passwd
+sudo sed -i '$ r sshd_config' /etc/ssh/sshd_config
 
 sudo mkdir -p /home/sshlogin/.ssh
 sudo chown sshlogin:root /home/sshlogin/.ssh

From 6a692253d405b2c2a2508035a7cd943576f08922 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Mon, 17 Feb 2020 00:15:26 +0100
Subject: [PATCH 10/37] Update

---
 nextcloud/crontab | 1 +
 1 file changed, 1 insertion(+)

diff --git a/nextcloud/crontab b/nextcloud/crontab
index a0fe9ba..5775c87 100644
--- a/nextcloud/crontab
+++ b/nextcloud/crontab
@@ -1 +1,2 @@
 */5 *	* * *	php nextcloud/cron.php
+15 3	* * *	systemd-tmpfiles --clean --user

From d78ca1203fb0c624129bdf2ef728086b6a322971 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Mon, 17 Feb 2020 00:24:33 +0100
Subject: [PATCH 11/37] Update

---
 nginx/README.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/nginx/README.md b/nginx/README.md
index 09c9170..fee1b7c 100644
--- a/nginx/README.md
+++ b/nginx/README.md
@@ -3,7 +3,7 @@
 ```sh
 sudo cp -r sites-available snippets conf.d /etc/nginx
 
-cd /etc/nginx/sites-enabled
-sudo ln -s ../sites-available/0nohost
-sudo ln -s ../sites-available/redirect-ssl-all
+sudo rm /etc/nginx/sites-*/default
+sudo ln -s ../sites-available/0nohost /etc/nginx/sites-enabled
+sudo ln -s ../sites-available/redirect-ssl-all /etc/nginx/sites-enabled
 ```

From c1d7b063e2ab44ca0bbbdf1077d41c19f7aea450 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Mon, 17 Feb 2020 01:00:12 +0100
Subject: [PATCH 12/37] Update

---
 mail/README.md           | 11 ++++++++---
 mail/dkim.sql            |  2 ++
 mail/opendkim/local.conf |  4 ----
 3 files changed, 10 insertions(+), 7 deletions(-)
 create mode 100644 mail/dkim.sql

diff --git a/mail/README.md b/mail/README.md
index a7b1410..15293c3 100644
--- a/mail/README.md
+++ b/mail/README.md
@@ -26,12 +26,17 @@ sudo apt install sqlite3 postfix postfix-sqlite dovecot-imapd dovecot-sqlite ope
 ```sh
 DOMAIN=example.com
 
-sudo ln -s /data/mail/config/vmail.db /.opendkim-bug-241.db
-sudo opendkim-genkey -D /data/mail -d $DOMAIN -s s
-
 sudo cp -r postfix dovecot /etc
 sudo sed -i '$ r opendkim/local.conf' /etc/opendkim.conf
 sudo sed -i s/example.com/$DOMAIN/g /etc/postfix/main.cf /etc/dovecot/local.conf
+
+sudo ln -s /data/mail/config/vmail.db /.opendkim-bug-241.db
+
+opendkim-genkey -d $DOMAIN -s s
+chmod +r s.private
+cat dkim.sql | sed s/DOMAIN/$DOMAIN/ | sudo -u vmail sqlite3 /data/mail/config/vmail.db
+cat s.txt
+rm s.private s.txt
 ```
 
 ## Notes
diff --git a/mail/dkim.sql b/mail/dkim.sql
new file mode 100644
index 0000000..c0e58e8
--- /dev/null
+++ b/mail/dkim.sql
@@ -0,0 +1,2 @@
+insert into dkim (match, key) values ("DOMAIN", "default");
+insert into dkim_key (key, domain, selector, private_key) values ("default", "DOMAIN", "s", readfile("s.private"));
diff --git a/mail/opendkim/local.conf b/mail/opendkim/local.conf
index ebfd7ed..58f5544 100644
--- a/mail/opendkim/local.conf
+++ b/mail/opendkim/local.conf
@@ -6,7 +6,3 @@ Canonicalization relaxed/relaxed
 
 KeyTable dsn:sqlite://./opendkim-bug-241.db/table=dkim_key?keycol=key?datacol=domain,selector,private_key
 SigningTable dsn:sqlite://./opendkim-bug-241.db/table=dkim?keycol=match?datacol=key
-
-# Domain example.com
-# Selector s
-# KeyFile /data/mail/s.private

From 50c6e9f10501e3be342aa3cf30e17d7f70f49c1d Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Mon, 17 Feb 2020 01:46:24 +0100
Subject: [PATCH 13/37] Update

---
 inbucket/README.md       | 2 +-
 mail/README.md           | 6 +++---
 mail/opendkim/local.conf | 4 ++--
 mail/postfix/main.cf     | 2 +-
 nginx/conf.d/ssl.conf    | 4 ++--
 5 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/inbucket/README.md b/inbucket/README.md
index 2807b25..ddc21f9 100644
--- a/inbucket/README.md
+++ b/inbucket/README.md
@@ -20,8 +20,8 @@ sudo ln -s ../share/inbucket/inbucket-client /usr/local/bin
 DOMAIN=test.local
 
 sudo cp inbucket.service /etc/systemd/system
-sudo -u vmail cp inbucket.conf /data/mail/config
 sudo cp nginx.conf /etc/nginx/sites-available/inbucket
+sudo -u vmail cp inbucket.conf /data/mail/config
 
 sudo ln -s ../sites-available/inbucket /etc/nginx/sites-enabled
 
diff --git a/mail/README.md b/mail/README.md
index 15293c3..2c8b391 100644
--- a/mail/README.md
+++ b/mail/README.md
@@ -4,21 +4,20 @@
 
 ```sh
 sudo sed -i '$ a vmail:*:2000:2000::/data/mail:/usr/sbin/nologin' /etc/passwd
-sudo sed -i '$ a vmail:x:2000:' /etc/passwd
+sudo sed -i '$ a vmail:x:2000:' /etc/group
 
 sudo mkdir -p /data/mail/mail
 sudo mkdir -p /data/mail/config
 sudo chown vmail: /data/mail/*
 
 cat schema.sql | sudo -u vmail sqlite3 /data/mail/config/vmail.db
-sudo chown vmail:postfix /data/mail/config/vmail.db
 sudo chmod 640 /data/mail/config/vmail.db
 ```
 
 ## Install Software
 
 ```sh
-sudo apt install sqlite3 postfix postfix-sqlite dovecot-imapd dovecot-sqlite opendkim
+sudo apt install sqlite3 postfix postfix-sqlite dovecot-imapd dovecot-sqlite opendkim libopendbx1-sqlite3
 ```
 
 ## Apply Configuration
@@ -31,6 +30,7 @@ sudo sed -i '$ r opendkim/local.conf' /etc/opendkim.conf
 sudo sed -i s/example.com/$DOMAIN/g /etc/postfix/main.cf /etc/dovecot/local.conf
 
 sudo ln -s /data/mail/config/vmail.db /.opendkim-bug-241.db
+sudo chown vmail:postfix /data/mail/config/vmail.db
 
 opendkim-genkey -d $DOMAIN -s s
 chmod +r s.private
diff --git a/mail/opendkim/local.conf b/mail/opendkim/local.conf
index 58f5544..72a9dc7 100644
--- a/mail/opendkim/local.conf
+++ b/mail/opendkim/local.conf
@@ -4,5 +4,5 @@ InternalHosts 0.0.0.0/0
 
 Canonicalization relaxed/relaxed
 
-KeyTable dsn:sqlite://./opendkim-bug-241.db/table=dkim_key?keycol=key?datacol=domain,selector,private_key
-SigningTable dsn:sqlite://./opendkim-bug-241.db/table=dkim?keycol=match?datacol=key
+KeyTable dsn:sqlite3://./opendkim-bug-241.db/table=dkim_key?keycol=key?datacol=domain,selector,private_key
+SigningTable dsn:sqlite3://./opendkim-bug-241.db/table=dkim?keycol=match?datacol=key
diff --git a/mail/postfix/main.cf b/mail/postfix/main.cf
index 8b60399..4b2e97f 100644
--- a/mail/postfix/main.cf
+++ b/mail/postfix/main.cf
@@ -5,7 +5,7 @@ compatibility_level = 2
 disable_vrfy_command = yes
 mailbox_size_limit = 0
 message_size_limit = 0
-mydomain = example.com
+mydomain = local
 mynetworks_style = subnet
 
 # TLS
diff --git a/nginx/conf.d/ssl.conf b/nginx/conf.d/ssl.conf
index 9171037..c68755d 100644
--- a/nginx/conf.d/ssl.conf
+++ b/nginx/conf.d/ssl.conf
@@ -1,2 +1,2 @@
-ssl_certificate /path/to/fullchain.pem;
-ssl_certificate_key /path/to/privkey.pem;
+ssl_certificate /data/ssl/certs/any.example.com/fullchain.pem;
+ssl_certificate_key /data/ssl/certs/any.example.com/privkey.pem;

From 54b4e991514354a1ca4022d6ec1894606d8a0ec1 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sat, 22 Feb 2020 11:47:47 +0100
Subject: [PATCH 14/37] Update

---
 gitea/README.md           | 40 +++++++++++++++++++++++++++++++++++++++
 gitea/gitea.service       | 10 ++++++++++
 gitea/nginx.conf          | 10 ++++++++++
 inbucket/README.md        |  1 +
 iodine/README.md          | 32 +++++++++++++++++++++++++++++++
 iodine/my-iodined.conf    |  2 ++
 iodine/my-iodined.service | 11 +++++++++++
 iptables/README.md        |  7 +++++++
 iptables/default.rules    | 17 +++++++++++++++++
 iptables/empty.rules      | 10 ++++++++++
 iptables/iptables.service | 13 +++++++++++++
 mail/README.md            |  3 ++-
 nextcloud/README.md       |  3 +--
 restic/README.md          |  2 +-
 ssh/README.md             |  2 +-
 15 files changed, 158 insertions(+), 5 deletions(-)
 create mode 100644 gitea/README.md
 create mode 100644 gitea/gitea.service
 create mode 100644 gitea/nginx.conf
 create mode 100644 iodine/README.md
 create mode 100644 iodine/my-iodined.conf
 create mode 100644 iodine/my-iodined.service
 create mode 100644 iptables/README.md
 create mode 100644 iptables/default.rules
 create mode 100644 iptables/empty.rules
 create mode 100644 iptables/iptables.service

diff --git a/gitea/README.md b/gitea/README.md
new file mode 100644
index 0000000..645794a
--- /dev/null
+++ b/gitea/README.md
@@ -0,0 +1,40 @@
+# Gitea
+
+## Create User
+
+```sh
+sudo sed -i '$ a git:*:2050:2050::/data/git:/bin/sh' /etc/passwd
+sudo sed -i '$ a git:x:2050:' /etc/group
+
+sudo mkdir -p /data/git/gitea
+sudo chown -R git: /data/git
+```
+
+## Install Software
+
+```sh
+sudo apt install git
+sudo -u git wget -O /data/git/gitea/gitea https://dl.gitea.io/gitea/...
+sudo -u git chmod +x /data/git/gitea/gitea
+```
+
+## Apply Configuration
+
+```sh
+DOMAIN=example.com
+
+sudo cp nginx.conf /etc/nginx/sites-available/git
+sudo cp gitea.service /etc/systemd/system
+
+sudo systemctl enable gitea
+sudo systemctl start gitea
+
+sudo sed -i s/example.com/$DOMAIN/ /etc/nginx/sites-available/git
+
+sudo ln -s ../sites-available/git /etc/nginx/sites-enabled
+sudo nginx -s reload
+
+sudo -u git sed -i /MODE.*file/s/file/console/ /data/git/gitea/custom/conf/app.ini
+sudo -u git sed -i /LEVEL.*info/s/info/warn/ /data/git/gitea/custom/conf/app.ini
+sudo -u git /data/git/gitea/gitea admin create-user --username admin --password admin --admin --email admin
+```
diff --git a/gitea/gitea.service b/gitea/gitea.service
new file mode 100644
index 0000000..4bc4533
--- /dev/null
+++ b/gitea/gitea.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=Gitea
+After=network.target
+
+[Service]
+ExecStart=/data/git/gitea/gitea web
+User=git
+
+[Install]
+WantedBy=multi-user.target
diff --git a/gitea/nginx.conf b/gitea/nginx.conf
new file mode 100644
index 0000000..b0af6b5
--- /dev/null
+++ b/gitea/nginx.conf
@@ -0,0 +1,10 @@
+server {
+	server_name git.example.com;
+
+	listen 443 ssl;
+
+	location / {
+		proxy_pass http://localhost:3000;
+		include proxy_params;
+	}
+}
diff --git a/inbucket/README.md b/inbucket/README.md
index ddc21f9..e190c48 100644
--- a/inbucket/README.md
+++ b/inbucket/README.md
@@ -24,6 +24,7 @@ sudo cp nginx.conf /etc/nginx/sites-available/inbucket
 sudo -u vmail cp inbucket.conf /data/mail/config
 
 sudo ln -s ../sites-available/inbucket /etc/nginx/sites-enabled
+sudo sed -i s/example.com/$DOMAIN/ /etc/nginx/sites-available/inbucket
 
 echo "inbucket:$(openssl passwd -5)" | sudo -u vmail tee /data/mail/config/inbucket.htpasswd
 sudo chown vmail:www-data /data/mail/config/inbucket.htpasswd
diff --git a/iodine/README.md b/iodine/README.md
new file mode 100644
index 0000000..4fd3392
--- /dev/null
+++ b/iodine/README.md
@@ -0,0 +1,32 @@
+# Iodine
+
+## Install Software
+
+```sh
+sudo apt install iodine
+```
+
+## Apply Configuration
+
+```sh
+EXTERNAL=eth0
+INTERNAL=dns0
+
+echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
+echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/60-ipv4-forward.conf
+
+sudo iptables -t nat -A POSTROUTING -o $EXTERNAL -j MASQUERADE
+sudo iptables -A INPUT -p udp --dport 5353 -j ACCEPT
+sudo iptables -A FORWARD -i $EXTERNAL -o $INTERNAL -m state --state RELATED,ESTABLISHED -j ACCEPT
+sudo iptables -A FORWARD -i $INTERNAL -o $EXTERNAL -j ACCEPT
+
+# Adjust domain:
+sudo iptables -t nat -A PREROUTING -p udp -m udp --dport 53 -m string --hex-string "|01|t|07|example|03|com|00|" --algo bm --from 20 --to 65535 -j REDIRECT --to-ports 5353
+
+sudo cp my-iodine.service /etc/systemd/system
+sudo cp my-iodined.conf /etc
+sudo chmod 600 /etc/iodined.conf
+
+sudo editor /etc/my-iodined.conf
+sudo systemctl enable my-iodine
+```
diff --git a/iodine/my-iodined.conf b/iodine/my-iodined.conf
new file mode 100644
index 0000000..d454d38
--- /dev/null
+++ b/iodine/my-iodined.conf
@@ -0,0 +1,2 @@
+IODINED_OPTS="-c -p 5353 192.168.100.1 t.example.com"
+IODINED_PASS=secret
diff --git a/iodine/my-iodined.service b/iodine/my-iodined.service
new file mode 100644
index 0000000..738fa2c
--- /dev/null
+++ b/iodine/my-iodined.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Iodine
+After=network.target
+
+[Service]
+EnvironmentFile=/etc/my-iodined.conf
+ExecStartPre=mkdir -p /run/iodined
+ExecStart=iodined -f -u nobody -t /run/iodined $IODINED_OPTS
+
+[Install]
+WantedBy=multi-user.target
diff --git a/iptables/README.md b/iptables/README.md
new file mode 100644
index 0000000..f9d2317
--- /dev/null
+++ b/iptables/README.md
@@ -0,0 +1,7 @@
+# Iptables
+
+```sh
+sudo cp *.rules /etc
+sudo cp *.service /etc/systemd/system
+sudo systemctl enable iptables
+```
diff --git a/iptables/default.rules b/iptables/default.rules
new file mode 100644
index 0000000..9332939
--- /dev/null
+++ b/iptables/default.rules
@@ -0,0 +1,17 @@
+*filter
+:SERVICES -
+-A INPUT -i lo -j ACCEPT
+-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
+-A INPUT -j SERVICES
+-A SERVICES -p tcp -m tcp --dport 143 -j ACCEPT
+-A SERVICES -p tcp -m tcp --dport 25 -j ACCEPT
+-A SERVICES -p tcp -m tcp --dport 443 -j ACCEPT
+-A SERVICES -p tcp -m tcp --dport 80 -j ACCEPT
+-A SERVICES -p tcp -m tcp --dport 22222 -j ACCEPT
+-A SERVICES -p udp -m udp --dport 53 -j ACCEPT
+-A INPUT -j DROP
+COMMIT
+
+*nat
+-A PREROUTING -p tcp -m tcp --dport 4986 -j REDIRECT --to-ports 22222
+COMMIT
diff --git a/iptables/empty.rules b/iptables/empty.rules
new file mode 100644
index 0000000..8f9f18b
--- /dev/null
+++ b/iptables/empty.rules
@@ -0,0 +1,10 @@
+*filter
+COMMIT
+*nat
+COMMIT
+*mangle
+COMMIT
+*raw
+COMMIT
+*security
+COMMIT
diff --git a/iptables/iptables.service b/iptables/iptables.service
new file mode 100644
index 0000000..45847f3
--- /dev/null
+++ b/iptables/iptables.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=iptables
+Before=network-pre.target
+Wants=network-pre.target
+
+[Service]
+Type=oneshot
+ExecStart=iptables-restore /etc/default.rules
+ExecStop=iptables-restore /etc/empty.rules
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
diff --git a/mail/README.md b/mail/README.md
index 2c8b391..1119549 100644
--- a/mail/README.md
+++ b/mail/README.md
@@ -27,7 +27,8 @@ DOMAIN=example.com
 
 sudo cp -r postfix dovecot /etc
 sudo sed -i '$ r opendkim/local.conf' /etc/opendkim.conf
-sudo sed -i s/example.com/$DOMAIN/g /etc/postfix/main.cf /etc/dovecot/local.conf
+sudo sed -i s/example.com/$DOMAIN/ /etc/postfix/main.cf /etc/dovecot/local.conf
+sudo sed -i '/include auth-system/ s/.*/#&/' /etc/dovecot/conf.d/10-auth.conf
 
 sudo ln -s /data/mail/config/vmail.db /.opendkim-bug-241.db
 sudo chown vmail:postfix /data/mail/config/vmail.db
diff --git a/nextcloud/README.md b/nextcloud/README.md
index 23e88ce..69888f3 100644
--- a/nextcloud/README.md
+++ b/nextcloud/README.md
@@ -33,9 +33,8 @@ sudo sed -i '$ r redis.conf' /etc/redis/redis.conf
 sudo -u cloud cp local.config.php /data/cloud/nextcloud/config
 sudo -u cloud crontab crontab
 
-sudo sed -i s/example.com/$DOMAIN/g /etc/nginx/sites-available/nextcloud /data/cloud/nextcloud/config/local.config.php
-
 sudo ln -s ../sites-available/nextcloud /etc/nginx/sites-enabled
+sudo sed -i s/example.com/$DOMAIN/ /etc/nginx/sites-available/nextcloud /data/cloud/nextcloud/config/local.config.php
 
 sudo -u cloud mkdir -p /data/cloud/.config/user-tmpfiles.d
 echo 'e %h/data/*/files/tmp - - - 7d' | sudo -u cloud tee /data/cloud/.config/user-tmpfiles.d/nextcloud-tmp.conf
diff --git a/restic/README.md b/restic/README.md
index 3c23001..3cbb4ee 100644
--- a/restic/README.md
+++ b/restic/README.md
@@ -13,7 +13,7 @@ echo 'nice /usr/local/bin/restic -r' "$REPO" '-p /root/backup-key "$@"' | sudo t
 sudo chmod +x /usr/local/bin/restic-cmd
 
 cat /dev/urandom | base64 | head -c 64 | sudo tee /root/backup-key
-sudo chmod 0600 /root/backup-key
+sudo chmod 600 /root/backup-key
 
 sudo restic-cmd init
 sudo crontab crontab
diff --git a/ssh/README.md b/ssh/README.md
index fdda9eb..a0e5964 100644
--- a/ssh/README.md
+++ b/ssh/README.md
@@ -12,4 +12,4 @@ sudo chown sshlogin:root /home/sshlogin/.ssh
 ```
 
 * Either create a password with `sudo passwd sshlogin` or
-* Add a key `sudo -u sshlogin nano /home/sshlogin/.ssh/authorized_keys`
+* Add a key `sudo -u sshlogin editor /home/sshlogin/.ssh/authorized_keys`

From de5eb91e91dd0c4ba7d023663525f4ee1b8f7ebf Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sun, 23 Feb 2020 19:47:53 +0100
Subject: [PATCH 15/37] Update

---
 gitea/README.md           |  3 +++
 iodine/README.md          | 12 ++++++++----
 iodine/my-iodined.service |  2 +-
 iptables/README.md        | 26 ++++++++++++++++++++++++++
 iptables/default.rules    | 27 +++++++++++++--------------
 iptables/empty.rules      |  1 +
 iptables/ratelimit.rules  | 16 ++++++++++++++++
 7 files changed, 68 insertions(+), 19 deletions(-)
 create mode 100644 iptables/ratelimit.rules

diff --git a/gitea/README.md b/gitea/README.md
index 645794a..6fbb691 100644
--- a/gitea/README.md
+++ b/gitea/README.md
@@ -34,6 +34,9 @@ sudo sed -i s/example.com/$DOMAIN/ /etc/nginx/sites-available/git
 sudo ln -s ../sites-available/git /etc/nginx/sites-enabled
 sudo nginx -s reload
 
+
+sudo iptables -t nat -A PREROUTING -p tcp --dport 4986 -j REDIRECT --to-ports 22222
+
 sudo -u git sed -i /MODE.*file/s/file/console/ /data/git/gitea/custom/conf/app.ini
 sudo -u git sed -i /LEVEL.*info/s/info/warn/ /data/git/gitea/custom/conf/app.ini
 sudo -u git /data/git/gitea/gitea admin create-user --username admin --password admin --admin --email admin
diff --git a/iodine/README.md b/iodine/README.md
index 4fd3392..3c2d6bb 100644
--- a/iodine/README.md
+++ b/iodine/README.md
@@ -17,16 +17,20 @@ echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/60-ipv4-forward.conf
 
 sudo iptables -t nat -A POSTROUTING -o $EXTERNAL -j MASQUERADE
 sudo iptables -A INPUT -p udp --dport 5353 -j ACCEPT
+sudo iptables -A INPUT -i $INTERNAL -j ACCEPT
+
+# Necessary only if default policy is not ACCEPT
 sudo iptables -A FORWARD -i $EXTERNAL -o $INTERNAL -m state --state RELATED,ESTABLISHED -j ACCEPT
 sudo iptables -A FORWARD -i $INTERNAL -o $EXTERNAL -j ACCEPT
 
 # Adjust domain:
-sudo iptables -t nat -A PREROUTING -p udp -m udp --dport 53 -m string --hex-string "|01|t|07|example|03|com|00|" --algo bm --from 20 --to 65535 -j REDIRECT --to-ports 5353
+sudo iptables -t nat -A PREROUTING -p udp --dport 53 -m string --hex-string "|01|t|07|example|03|com|00|" --algo bm --from 20 --to 65535 -j REDIRECT --to-ports 5353
 
-sudo cp my-iodine.service /etc/systemd/system
+sudo cp my-iodined.service /etc/systemd/system
 sudo cp my-iodined.conf /etc
-sudo chmod 600 /etc/iodined.conf
+sudo chmod 600 /etc/my-iodined.conf
 
 sudo editor /etc/my-iodined.conf
-sudo systemctl enable my-iodine
+sudo systemctl enable my-iodined
+sudo systemctl start my-iodined
 ```
diff --git a/iodine/my-iodined.service b/iodine/my-iodined.service
index 738fa2c..953abfc 100644
--- a/iodine/my-iodined.service
+++ b/iodine/my-iodined.service
@@ -1,6 +1,6 @@
 [Unit]
 Description=Iodine
-After=network.target
+After=network-online.target
 
 [Service]
 EnvironmentFile=/etc/my-iodined.conf
diff --git a/iptables/README.md b/iptables/README.md
index f9d2317..3e0dbd7 100644
--- a/iptables/README.md
+++ b/iptables/README.md
@@ -5,3 +5,29 @@ sudo cp *.rules /etc
 sudo cp *.service /etc/systemd/system
 sudo systemctl enable iptables
 ```
+
+## Apply and Report Rate Limits
+
+The `ratelimit.rules` file adds new chains to rate limit subnets.
+
+```sh
+sudo iptables-restore -n < ratelimit.rules
+
+# Common offenders
+sudo iptables -t raw -A RATELIMIT_SUBNET -s 185.0.0.0/8 -j RATELIMIT_SUBNET_DEFAULT
+sudo iptables -t raw -A RATELIMIT_SUBNET -s 45.0.0.0/8 -j RATELIMIT_SUBNET_DEFAULT
+sudo iptables -t raw -A RATELIMIT_SUBNET -s 193.0.0.0/8 -j RATELIMIT_SUBNET_DEFAULT
+
+# Default action
+sudo iptables -t raw -A RATELIMIT_SUBNET_DEFAULT -p tcp --tcp-flags SYN,ACK SYN \
+ -m hashlimit --hashlimit-name drop_4h \
+ --hashlimit-above 4/hour --hashlimit-burst 2 \
+ --hashlimit-mode srcip,dstport --hashlimit-srcmask 16 -j DROP
+
+# Log potential offenders
+sudo iptables -t raw -A RATELIMIT_REPORT -p tcp --tcp-flags SYN,ACK SYN \
+ -m hashlimit --hashlimit-name log_1s_burst4_net16 \
+ --hashlimit-above 1/second --hashlimit-burst 4 \
+ --hashlimit-mode srcip,dstport --hashlimit-srcmask 16 -j LOG \
+ --log-level 5 --log-prefix "ratelimit above 1/second burst 4 srcmask 16 "
+```
diff --git a/iptables/default.rules b/iptables/default.rules
index 9332939..a07b16d 100644
--- a/iptables/default.rules
+++ b/iptables/default.rules
@@ -1,17 +1,16 @@
 *filter
-:SERVICES -
--A INPUT -i lo -j ACCEPT
--A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
--A INPUT -j SERVICES
--A SERVICES -p tcp -m tcp --dport 143 -j ACCEPT
--A SERVICES -p tcp -m tcp --dport 25 -j ACCEPT
--A SERVICES -p tcp -m tcp --dport 443 -j ACCEPT
--A SERVICES -p tcp -m tcp --dport 80 -j ACCEPT
--A SERVICES -p tcp -m tcp --dport 22222 -j ACCEPT
--A SERVICES -p udp -m udp --dport 53 -j ACCEPT
+:INPUT DROP
+:CONNECTION -
+:SERVICE -
+-A INPUT -j CONNECTION
+-A INPUT -j SERVICE
 -A INPUT -j DROP
-COMMIT
-
-*nat
--A PREROUTING -p tcp -m tcp --dport 4986 -j REDIRECT --to-ports 22222
+-A CONNECTION -i lo -j ACCEPT
+-A CONNECTION -m state --state RELATED,ESTABLISHED -j ACCEPT
+-A SERVICE -p tcp --dport 25 -j ACCEPT
+-A SERVICE -p tcp --dport 80 -j ACCEPT
+-A SERVICE -p tcp --dport 143 -j ACCEPT
+-A SERVICE -p tcp --dport 443 -j ACCEPT
+-A SERVICE -p tcp --dport 22222 -j ACCEPT
+-A SERVICE -p udp --dport 53 -j ACCEPT
 COMMIT
diff --git a/iptables/empty.rules b/iptables/empty.rules
index 8f9f18b..7a7d32c 100644
--- a/iptables/empty.rules
+++ b/iptables/empty.rules
@@ -1,4 +1,5 @@
 *filter
+:INPUT ACCEPT
 COMMIT
 *nat
 COMMIT
diff --git a/iptables/ratelimit.rules b/iptables/ratelimit.rules
new file mode 100644
index 0000000..12bfaf2
--- /dev/null
+++ b/iptables/ratelimit.rules
@@ -0,0 +1,16 @@
+*raw
+:RATELIMIT -
+:RATELIMIT_REPORT -
+:RATELIMIT_SUBNET -
+:RATELIMIT_SUBNET_DEFAULT -
+
+-I PREROUTING -j RATELIMIT
+
+-A RATELIMIT -s 127.0.0.0/8 -j RETURN
+-A RATELIMIT -s 10.0.0.0/8 -j RETURN
+-A RATELIMIT -s 172.16.0.0/12 -j RETURN
+-A RATELIMIT -s 192.168.0.0/16 -j RETURN
+-A RATELIMIT -j RATELIMIT_SUBNET
+-A RATELIMIT -j RATELIMIT_REPORT
+
+COMMIT

From 18c7a935a7f9c3f8d95b6160ea2b7796178a12f0 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sun, 23 Feb 2020 19:52:37 +0100
Subject: [PATCH 16/37] Update

---
 iptables/default.rules | 1 -
 1 file changed, 1 deletion(-)

diff --git a/iptables/default.rules b/iptables/default.rules
index a07b16d..f6c250b 100644
--- a/iptables/default.rules
+++ b/iptables/default.rules
@@ -4,7 +4,6 @@
 :SERVICE -
 -A INPUT -j CONNECTION
 -A INPUT -j SERVICE
--A INPUT -j DROP
 -A CONNECTION -i lo -j ACCEPT
 -A CONNECTION -m state --state RELATED,ESTABLISHED -j ACCEPT
 -A SERVICE -p tcp --dport 25 -j ACCEPT

From b23946b81c664f50076ac839d0a39d1f57aa546b Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sun, 23 Feb 2020 20:02:32 +0100
Subject: [PATCH 17/37] Update

---
 iptables/README.md       | 12 ++++++------
 iptables/ratelimit.rules |  2 +-
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/iptables/README.md b/iptables/README.md
index 3e0dbd7..3e2a65d 100644
--- a/iptables/README.md
+++ b/iptables/README.md
@@ -14,20 +14,20 @@ The `ratelimit.rules` file adds new chains to rate limit subnets.
 sudo iptables-restore -n < ratelimit.rules
 
 # Common offenders
-sudo iptables -t raw -A RATELIMIT_SUBNET -s 185.0.0.0/8 -j RATELIMIT_SUBNET_DEFAULT
-sudo iptables -t raw -A RATELIMIT_SUBNET -s 45.0.0.0/8 -j RATELIMIT_SUBNET_DEFAULT
-sudo iptables -t raw -A RATELIMIT_SUBNET -s 193.0.0.0/8 -j RATELIMIT_SUBNET_DEFAULT
+sudo iptables -t raw -A RATELIMIT_SUBNET -s 185.0.0.0/8 -j RATELIMIT_DEFAULT
+sudo iptables -t raw -A RATELIMIT_SUBNET -s 45.0.0.0/8 -j RATELIMIT_DEFAULT
+sudo iptables -t raw -A RATELIMIT_SUBNET -s 193.0.0.0/8 -j RATELIMIT_DEFAULT
 
 # Default action
-sudo iptables -t raw -A RATELIMIT_SUBNET_DEFAULT -p tcp --tcp-flags SYN,ACK SYN \
+sudo iptables -t raw -A RATELIMIT_DEFAULT -p tcp --tcp-flags SYN,ACK SYN \
  -m hashlimit --hashlimit-name drop_4h \
  --hashlimit-above 4/hour --hashlimit-burst 2 \
  --hashlimit-mode srcip,dstport --hashlimit-srcmask 16 -j DROP
 
 # Log potential offenders
 sudo iptables -t raw -A RATELIMIT_REPORT -p tcp --tcp-flags SYN,ACK SYN \
- -m hashlimit --hashlimit-name log_1s_burst4_net16 \
+ -m hashlimit --hashlimit-name report1 \
  --hashlimit-above 1/second --hashlimit-burst 4 \
  --hashlimit-mode srcip,dstport --hashlimit-srcmask 16 -j LOG \
- --log-level 5 --log-prefix "ratelimit above 1/second burst 4 srcmask 16 "
+ --log-level 5 --log-prefix "ratelimit report1 "
 ```
diff --git a/iptables/ratelimit.rules b/iptables/ratelimit.rules
index 12bfaf2..e7dfdc9 100644
--- a/iptables/ratelimit.rules
+++ b/iptables/ratelimit.rules
@@ -2,7 +2,7 @@
 :RATELIMIT -
 :RATELIMIT_REPORT -
 :RATELIMIT_SUBNET -
-:RATELIMIT_SUBNET_DEFAULT -
+:RATELIMIT_DEFAULT -
 
 -I PREROUTING -j RATELIMIT
 

From c94ed5a66ad601e065a845a1c1742f56b013397b Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sun, 23 Feb 2020 21:23:02 +0100
Subject: [PATCH 18/37] Update

---
 restic/README.md | 6 +++---
 restic/crontab   | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/restic/README.md b/restic/README.md
index 3cbb4ee..2f94664 100644
--- a/restic/README.md
+++ b/restic/README.md
@@ -9,12 +9,12 @@ REPO=sftp:backup-user@example.com:repo
 bunzip2 restic*.bz2
 sudo cp restic* /usr/local/bin/restic
 
-echo 'nice /usr/local/bin/restic -r' "$REPO" '-p /root/backup-key "$@"' | sudo tee /usr/local/bin/restic-cmd
-sudo chmod +x /usr/local/bin/restic-cmd
+echo 'nice /usr/local/bin/restic -r' "$REPO" '-p /root/backup-key "$@"' | sudo tee /root/restic-cmd
+sudo chmod +x /root/restic-cmd
 
 cat /dev/urandom | base64 | head -c 64 | sudo tee /root/backup-key
 sudo chmod 600 /root/backup-key
 
-sudo restic-cmd init
+sudo /root/restic-cmd init
 sudo crontab crontab
 ```
diff --git a/restic/crontab b/restic/crontab
index 69bc4f2..5d2bebc 100644
--- a/restic/crontab
+++ b/restic/crontab
@@ -1,2 +1,2 @@
-48 *	* * *	/usr/local/bin/restic-cmd backup -q --exclude-if-present .nobackup /data
-18 3	* * *	/usr/local/bin/restic-cmd forget -q --keep-tag keep -H 24 -d 7 -m 12 -y 100
+48 *	* * *	/root/restic-cmd backup -q --exclude-if-present .nobackup /data
+18 3	* * *	/root/restic-cmd forget -q --keep-tag keep -H 24 -d 7 -m 12 -y 100

From 12b87e280591c189d4219d24cd36ed18c99fb8be Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Wed, 26 Feb 2020 01:14:32 +0100
Subject: [PATCH 19/37] Update

---
 ssh/sshd_config | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/ssh/sshd_config b/ssh/sshd_config
index 4db04db..fb33505 100644
--- a/ssh/sshd_config
+++ b/ssh/sshd_config
@@ -3,6 +3,9 @@ Port 22222
 AllowUsers sshlogin git backup-*
 ClientAliveInterval 10
 
+LoginGraceTime 10
+MaxAuthTries 2
+
 Match User backup-*
 	ForceCommand internal-sftp
 	ChrootDirectory %h

From 45708b28a0d97553d605648f3c1796e218643ee1 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sat, 7 Mar 2020 00:41:17 +0100
Subject: [PATCH 20/37] Update

---
 iptables/README.md       | 21 ++-------------------
 iptables/ratelimit.rules | 12 ++++++++++--
 2 files changed, 12 insertions(+), 21 deletions(-)

diff --git a/iptables/README.md b/iptables/README.md
index 3e2a65d..fc67e5c 100644
--- a/iptables/README.md
+++ b/iptables/README.md
@@ -8,26 +8,9 @@ sudo systemctl enable iptables
 
 ## Apply and Report Rate Limits
 
-The `ratelimit.rules` file adds new chains to rate limit subnets.
+The `ratelimit.rules` file adds new chains to
+limit the rate of new connections based on /16 subnets.
 
 ```sh
 sudo iptables-restore -n < ratelimit.rules
-
-# Common offenders
-sudo iptables -t raw -A RATELIMIT_SUBNET -s 185.0.0.0/8 -j RATELIMIT_DEFAULT
-sudo iptables -t raw -A RATELIMIT_SUBNET -s 45.0.0.0/8 -j RATELIMIT_DEFAULT
-sudo iptables -t raw -A RATELIMIT_SUBNET -s 193.0.0.0/8 -j RATELIMIT_DEFAULT
-
-# Default action
-sudo iptables -t raw -A RATELIMIT_DEFAULT -p tcp --tcp-flags SYN,ACK SYN \
- -m hashlimit --hashlimit-name drop_4h \
- --hashlimit-above 4/hour --hashlimit-burst 2 \
- --hashlimit-mode srcip,dstport --hashlimit-srcmask 16 -j DROP
-
-# Log potential offenders
-sudo iptables -t raw -A RATELIMIT_REPORT -p tcp --tcp-flags SYN,ACK SYN \
- -m hashlimit --hashlimit-name report1 \
- --hashlimit-above 1/second --hashlimit-burst 4 \
- --hashlimit-mode srcip,dstport --hashlimit-srcmask 16 -j LOG \
- --log-level 5 --log-prefix "ratelimit report1 "
 ```
diff --git a/iptables/ratelimit.rules b/iptables/ratelimit.rules
index e7dfdc9..db4319f 100644
--- a/iptables/ratelimit.rules
+++ b/iptables/ratelimit.rules
@@ -1,10 +1,10 @@
 *raw
 :RATELIMIT -
+:RATELIMIT_ENFORCE -
 :RATELIMIT_REPORT -
 :RATELIMIT_SUBNET -
-:RATELIMIT_DEFAULT -
 
--I PREROUTING -j RATELIMIT
+-I PREROUTING -p tcp --tcp-flags SYN,ACK SYN -j RATELIMIT
 
 -A RATELIMIT -s 127.0.0.0/8 -j RETURN
 -A RATELIMIT -s 10.0.0.0/8 -j RETURN
@@ -13,4 +13,12 @@
 -A RATELIMIT -j RATELIMIT_SUBNET
 -A RATELIMIT -j RATELIMIT_REPORT
 
+-A RATELIMIT_ENFORCE -m hashlimit --hashlimit-above 4/hour --hashlimit-burst 2 --hashlimit-mode srcip,dstport --hashlimit-name enforce --hashlimit-srcmask 16 -j DROP
+
+-A RATELIMIT_REPORT -m hashlimit --hashlimit-above 1/min --hashlimit-burst 6 --hashlimit-mode srcip,dstport --hashlimit-name report1 --hashlimit-srcmask 16 -j LOG --log-prefix "ratelimit report1 " --log-level 5
+
+-A RATELIMIT_SUBNET -s 185.0.0.0/8 -j RATELIMIT_ENFORCE
+-A RATELIMIT_SUBNET -s 45.0.0.0/8 -j RATELIMIT_ENFORCE
+-A RATELIMIT_SUBNET -s 193.0.0.0/8 -j RATELIMIT_ENFORCE
+
 COMMIT

From 80070352dedb8064ea66be64471eee9cc607842d Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sat, 7 Mar 2020 09:24:38 +0100
Subject: [PATCH 21/37] Update

---
 gitea/nginx.conf | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/gitea/nginx.conf b/gitea/nginx.conf
index b0af6b5..56a0b41 100644
--- a/gitea/nginx.conf
+++ b/gitea/nginx.conf
@@ -7,4 +7,6 @@ server {
 		proxy_pass http://localhost:3000;
 		include proxy_params;
 	}
+
+	location = /robots.txt { return 200 "User-agent: *\nDisallow: */commit/*\n"; }
 }

From 71d6a5fff3e4c0234b0fdf01cd306d1c607a3b78 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sat, 22 Feb 2020 14:20:30 +0100
Subject: [PATCH 22/37] Add NetworkManager configuration

---
 network-manager/99-no-wifi-on-ethernet | 11 +++++++++++
 network-manager/README.md              | 13 +++++++++++++
 2 files changed, 24 insertions(+)
 create mode 100755 network-manager/99-no-wifi-on-ethernet
 create mode 100644 network-manager/README.md

diff --git a/network-manager/99-no-wifi-on-ethernet b/network-manager/99-no-wifi-on-ethernet
new file mode 100755
index 0000000..fa44a2f
--- /dev/null
+++ b/network-manager/99-no-wifi-on-ethernet
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+logger -t no-wifi-on-ethernet "Device $1 is $2"
+
+if [ "dev:$1:$2" = "dev:eth0:up" ]; then
+	nmcli r wifi off
+fi
+
+if [ "dev:$1:$2" = "dev:eth0:down" ]; then
+	nmcli r wifi on
+fi
diff --git a/network-manager/README.md b/network-manager/README.md
new file mode 100644
index 0000000..6e60a35
--- /dev/null
+++ b/network-manager/README.md
@@ -0,0 +1,13 @@
+# NetworkManager
+
+## Manage ethernet devices with NetworkManager
+
+```sh
+touch /etc/NetworkManager/conf.d/10-globally-managed-devices.conf
+```
+
+## Automatically switch off wifi when ethernet is connected
+
+```sh
+sudo cp 99-no-wifi-on-ethernet /etc/NetworkManager/dispatcher.d
+```

From 4c004acda81255146c86399143973412ea9b8fbe Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Wed, 15 Apr 2020 23:22:14 +0200
Subject: [PATCH 23/37] Fix unprocessed index directives

---
 nextcloud/nginx.conf | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/nextcloud/nginx.conf b/nextcloud/nginx.conf
index 15307c4..8030b02 100644
--- a/nextcloud/nginx.conf
+++ b/nextcloud/nginx.conf
@@ -9,7 +9,6 @@ server {
 	client_max_body_size 0;
 
 	location / {
-		index index.php;
 		try_files $uri /index.php$request_uri;
 	}
 
@@ -26,6 +25,10 @@ server {
 		include fastcgi.conf;
 	}
 
+	location /updater { index index.php; }
+	location /ocm-provider { index index.php; }
+	location /ocs-provider { index index.php; }
+
 	location = /.well-known/carddav {
 		return 301 $scheme://$host:$server_port/remote.php/dav;
 	}

From 8b0615f9de0b9178f01d33e4e2953c9dcff0b5d2 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Mon, 13 Apr 2020 21:10:11 +0200
Subject: [PATCH 24/37] Add postfix SNI support

---
 mail/README.md       | 12 ++++++++++--
 mail/postfix/main.cf |  1 +
 mail/postfix/sni.cf  |  1 +
 3 files changed, 12 insertions(+), 2 deletions(-)
 create mode 100644 mail/postfix/sni.cf

diff --git a/mail/README.md b/mail/README.md
index 1119549..7945019 100644
--- a/mail/README.md
+++ b/mail/README.md
@@ -11,6 +11,7 @@ sudo mkdir -p /data/mail/config
 sudo chown vmail: /data/mail/*
 
 cat schema.sql | sudo -u vmail sqlite3 /data/mail/config/vmail.db
+sudo chown vmail:postfix /data/mail/config/vmail.db
 sudo chmod 640 /data/mail/config/vmail.db
 ```
 
@@ -26,12 +27,13 @@ sudo apt install sqlite3 postfix postfix-sqlite dovecot-imapd dovecot-sqlite ope
 DOMAIN=example.com
 
 sudo cp -r postfix dovecot /etc
+sudo chmod 600 /etc/postfix/sni.cf
+
 sudo sed -i '$ r opendkim/local.conf' /etc/opendkim.conf
-sudo sed -i s/example.com/$DOMAIN/ /etc/postfix/main.cf /etc/dovecot/local.conf
+sudo sed -i s/example.com/$DOMAIN/ /etc/postfix/{main,sni}.cf /etc/dovecot/local.conf
 sudo sed -i '/include auth-system/ s/.*/#&/' /etc/dovecot/conf.d/10-auth.conf
 
 sudo ln -s /data/mail/config/vmail.db /.opendkim-bug-241.db
-sudo chown vmail:postfix /data/mail/config/vmail.db
 
 opendkim-genkey -d $DOMAIN -s s
 chmod +r s.private
@@ -40,6 +42,12 @@ cat s.txt
 rm s.private s.txt
 ```
 
+## Certificate Reload
+
+```sh
+postmap -F /etc/postfix/sni.cf
+```
+
 ## Notes
 
 * The `vmail.db` parent directory needs to be writeable by the user modifying the database
diff --git a/mail/postfix/main.cf b/mail/postfix/main.cf
index 4b2e97f..66b0ba0 100644
--- a/mail/postfix/main.cf
+++ b/mail/postfix/main.cf
@@ -14,6 +14,7 @@ smtp_tls_security_level = may
 smtpd_tls_security_level = may
 smtpd_tls_key_file =  /data/ssl/certs/mail.example.com/privkey.pem
 smtpd_tls_cert_file = /data/ssl/certs/mail.example.com/fullchain.pem
+tls_server_sni_maps = hash:/etc/postfix/sni.cf
 
 # Custom
 
diff --git a/mail/postfix/sni.cf b/mail/postfix/sni.cf
new file mode 100644
index 0000000..e9ae8a0
--- /dev/null
+++ b/mail/postfix/sni.cf
@@ -0,0 +1 @@
+mail.example.com /data/ssl/certs/mail.example.com/privkey.pem /data/ssl/certs/mail.example.com/fullchain.pem

From f0dc028f270b3f374594f80b2985b321134b1918 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Thu, 16 Apr 2020 22:51:36 +0200
Subject: [PATCH 25/37] fixup-nginx

---
 nginx/README.md | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/nginx/README.md b/nginx/README.md
index fee1b7c..d87501e 100644
--- a/nginx/README.md
+++ b/nginx/README.md
@@ -1,9 +1,19 @@
 # Nginx
 
 ```sh
+DOMAIN=example.com
+
 sudo cp -r sites-available snippets conf.d /etc/nginx
 
 sudo rm /etc/nginx/sites-*/default
 sudo ln -s ../sites-available/0nohost /etc/nginx/sites-enabled
 sudo ln -s ../sites-available/redirect-ssl-all /etc/nginx/sites-enabled
+
+sudo sed -i s/example.com/$DOMAIN/ /etc/nginx/conf.d/ssl.conf
+```
+
+## Certificate Reload
+
+```sh
+nginx -s reload
 ```

From 0ba0c06653ead4827731bc77ab9b1b48310e80c9 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Thu, 16 Apr 2020 23:45:18 +0200
Subject: [PATCH 26/37] Add dyndns and letsencrypt helpers

---
 dyndns/README.md                | 16 +++++++++++++++
 dyndns/dyndns-nsupdate          | 10 +++++++++
 dyndns/dyndns-update            | 19 +++++++++++++++++
 dyndns/example.com.nsupdate.txt | 12 +++++++++++
 dyndns/tsig.example.com.conf    |  4 ++++
 dyndns/update-example.com.sh    |  7 +++++++
 letsencrypt/README.md           | 36 +++++++++++++++++++++++++++++++++
 letsencrypt/config              |  5 +++++
 letsencrypt/dehydrated-manual   | 11 ++++++++++
 letsencrypt/dehydrated-nsupdate | 24 ++++++++++++++++++++++
 letsencrypt/example-hook        |  7 +++++++
 11 files changed, 151 insertions(+)
 create mode 100644 dyndns/README.md
 create mode 100755 dyndns/dyndns-nsupdate
 create mode 100755 dyndns/dyndns-update
 create mode 100644 dyndns/example.com.nsupdate.txt
 create mode 100644 dyndns/tsig.example.com.conf
 create mode 100755 dyndns/update-example.com.sh
 create mode 100644 letsencrypt/README.md
 create mode 100644 letsencrypt/config
 create mode 100755 letsencrypt/dehydrated-manual
 create mode 100755 letsencrypt/dehydrated-nsupdate
 create mode 100644 letsencrypt/example-hook

diff --git a/dyndns/README.md b/dyndns/README.md
new file mode 100644
index 0000000..21e1264
--- /dev/null
+++ b/dyndns/README.md
@@ -0,0 +1,16 @@
+# Dynamic DNS
+
+Edit example files to match your needs.
+
+```sh
+sudo mkdir /data/dns
+cp *example* dyndns* /data/dns
+
+chmod 600 /data/dns/tsig*
+```
+
+## Cronjob
+
+```sh
+/data/dns/update-example.com.sh
+```
diff --git a/dyndns/dyndns-nsupdate b/dyndns/dyndns-nsupdate
new file mode 100755
index 0000000..1876f7c
--- /dev/null
+++ b/dyndns/dyndns-nsupdate
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+DYN_DIR=/data/dns
+
+if test "x$DYN_TSIGKEY" = x; then DYN_TSIGKEY="$DYN_DIR/tsig.$DYN_DOMAIN.conf"; fi
+if test "x$DYN_NSUPDATE" = x; then DYN_NSUPDATE="$DYN_DIR/$DYN_DOMAIN.nsupdate.txt"; fi
+
+if test "x$1" != x; then
+	cat "$DYN_NSUPDATE" | sed s/%IP%/$1/g | nsupdate -v -k "$DYN_TSIGKEY"
+fi
diff --git a/dyndns/dyndns-update b/dyndns/dyndns-update
new file mode 100755
index 0000000..2eb07c8
--- /dev/null
+++ b/dyndns/dyndns-update
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+if test "x$DYN_SERVER" = x; then echo export DYN_SERVER=ns.example.com; exit=1; fi
+if test "x$DYN_DOMAIN" = x; then echo export DYN_DOMAIN=example.com; exit=1; fi
+if test "x$DYN_SCRIPT" = x; then echo export DYN_SCRIPT=/path/to/script; exit=1; fi
+if test "x$exit" = x1; then exit 1; fi
+
+if test "x$DYN_IPAPI" = x; then DYN_IPAPI=ifconfig.co; fi
+
+IPACTUAL=$(wget -qO - "$DYN_IPAPI")
+IPSERVER=$(dig +short $DYN_DOMAIN @$DYN_SERVER)
+
+if test "x$IPSERVER" = x -o "x$IPACTUAL" = x; then
+	: # ERROR: IP unknown
+elif test "x$IPSERVER" = "x$IPACTUAL"; then
+	: # INFO: IP not changed
+else
+	"$DYN_SCRIPT" $IPACTUAL
+fi
diff --git a/dyndns/example.com.nsupdate.txt b/dyndns/example.com.nsupdate.txt
new file mode 100644
index 0000000..8153944
--- /dev/null
+++ b/dyndns/example.com.nsupdate.txt
@@ -0,0 +1,12 @@
+server ns01.example.com
+zone example.com
+
+update del   example.com. TXT
+update del   example.com. A
+update del *.example.com. A
+
+update add   example.com. 86400 TXT "v=spf1 ip4:%IP%/32 -all"
+update add   example.com. 86400 A %IP%
+update add *.example.com. 86400 A %IP%
+
+send
diff --git a/dyndns/tsig.example.com.conf b/dyndns/tsig.example.com.conf
new file mode 100644
index 0000000..1cea1d4
--- /dev/null
+++ b/dyndns/tsig.example.com.conf
@@ -0,0 +1,4 @@
+key "tsig.example.com." {
+	algorithm hmac-sha256;
+	secret "YWRyaXVtLmFkcml1bS4uCg==";
+};
diff --git a/dyndns/update-example.com.sh b/dyndns/update-example.com.sh
new file mode 100755
index 0000000..e30dd4e
--- /dev/null
+++ b/dyndns/update-example.com.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+export DYN_DOMAIN=example.com
+export DYN_SERVER=ns01.example.com
+export DYN_SCRIPT=/data/dns/dyndns-nsupdate
+
+/data/dns/dyndns-update
diff --git a/letsencrypt/README.md b/letsencrypt/README.md
new file mode 100644
index 0000000..c1f3f51
--- /dev/null
+++ b/letsencrypt/README.md
@@ -0,0 +1,36 @@
+# Let's Encrypt
+
+Download Let's Encrypt client (only `dehydrated` needed):
+https://github.com/dehydrated-io/dehydrated/releases/latest
+
+```sh
+sudo mkdir -p /data/ssl/{configs,challenge}
+sudo chown -R admin: /data/ssl
+
+cp config dehydrated-* /data/ssl
+
+# List all domains for automatic renewal
+editor /data/ssl/domains.txt
+
+/data/ssl/dehydrated -r
+```
+
+To enable certificate renewal,
+`include snippets/letsencrypt` or put `redirect-ssl-all` in sites-enabled.
+
+## Cronjob
+
+```sh
+/data/ssl/dehydrated -c
+```
+
+## Wildcard Certificates
+
+```sh
+echo "service.example.com *.service.example.com" >> /data/ssl/domains.txt
+echo "CHALLENGETYPE=dns-01" >> /data/ssl/configs/service.example.com
+echo "HOOK=/data/ssl/dehydrated-hook" >> /data/ssl/configs/service.example.com
+```
+
+There are manual and nsupdate hooks.
+See [example-hook](example-hook) for an example nsupdate hook.
diff --git a/letsencrypt/config b/letsencrypt/config
new file mode 100644
index 0000000..8ddd42e
--- /dev/null
+++ b/letsencrypt/config
@@ -0,0 +1,5 @@
+DOMAINS_D=/data/ssl/configs
+WELLKNOWN=/data/ssl/challenge
+PRIVATE_KEY_RENEW=no
+KEYSIZE=2048
+# CONTACT_EMAIL=hostmaster@example.com
diff --git a/letsencrypt/dehydrated-manual b/letsencrypt/dehydrated-manual
new file mode 100755
index 0000000..0436362
--- /dev/null
+++ b/letsencrypt/dehydrated-manual
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+if test "x$1" = xdeploy_challenge; then
+	echo "Add the following record and press enter to continue:"
+	echo "_acme-challenge.$2. TXT $4"
+	read dummy
+elif test "x$1" = xclean_challenge; then
+	echo "Remove the record and press enter to continue:"
+	echo "_acme-challenge.$2. TXT $4"
+	read dummy
+fi
diff --git a/letsencrypt/dehydrated-nsupdate b/letsencrypt/dehydrated-nsupdate
new file mode 100755
index 0000000..52fd241
--- /dev/null
+++ b/letsencrypt/dehydrated-nsupdate
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+SCRIPT_TTL=30
+
+if test "x$LE_SERVER" = x; then echo export LE_SERVER=ns.example.com; exit=1; fi
+if test "x$LE_ZONE" = x; then echo export LE_ZONE=example.com; exit=1; fi
+if test "x$LE_TSIGKEY" = x; then echo export LE_TSIGKEY=/path/to/key; exit=1; fi
+if test "x$exit" = x1; then exit 1; fi
+
+if test "x$1" = xdeploy_challenge; then
+	nsupdate -v -k "$LE_TSIGKEY" <<- NSUPDATE
+		server $LE_SERVER
+		zone $LE_ZONE
+		update add _acme-challenge.$2. $SCRIPT_TTL TXT $4
+		send
+	NSUPDATE
+elif test "x$1" = xclean_challenge; then
+	nsupdate -v -k "$LE_TSIGKEY" <<- NSUPDATE
+		server $LE_SERVER
+		zone $LE_ZONE
+		update del _acme-challenge.$2. TXT
+		send
+	NSUPDATE
+fi
diff --git a/letsencrypt/example-hook b/letsencrypt/example-hook
new file mode 100644
index 0000000..80a49ad
--- /dev/null
+++ b/letsencrypt/example-hook
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+export LE_TSIGKEY=/data/dns/tsig.example.com.conf
+export LE_SERVER=ns01.example.com
+export LE_ZONE=example.com
+
+/data/ssl/dehydrated-nsupdate "$@"

From 1f983af06dc1c33157b81c9505c2ad5563f40891 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Tue, 14 Apr 2020 00:43:49 +0200
Subject: [PATCH 27/37] Improve iptables ratelimit rules

---
 iptables/ratelimit.rules | 31 +++++++++++++++----------------
 1 file changed, 15 insertions(+), 16 deletions(-)

diff --git a/iptables/ratelimit.rules b/iptables/ratelimit.rules
index db4319f..c5dc31a 100644
--- a/iptables/ratelimit.rules
+++ b/iptables/ratelimit.rules
@@ -1,24 +1,23 @@
 *raw
+:BLOCK -
 :RATELIMIT -
-:RATELIMIT_ENFORCE -
-:RATELIMIT_REPORT -
-:RATELIMIT_SUBNET -
+:WHITELIST -
 
--I PREROUTING -p tcp --tcp-flags SYN,ACK SYN -j RATELIMIT
+-A PREROUTING -j WHITELIST
+-A PREROUTING -j BLOCK
+-A PREROUTING -p tcp -m tcp --tcp-flags SYN,ACK SYN -j RATELIMIT
 
--A RATELIMIT -s 127.0.0.0/8 -j RETURN
--A RATELIMIT -s 10.0.0.0/8 -j RETURN
--A RATELIMIT -s 172.16.0.0/12 -j RETURN
--A RATELIMIT -s 192.168.0.0/16 -j RETURN
--A RATELIMIT -j RATELIMIT_SUBNET
--A RATELIMIT -j RATELIMIT_REPORT
+-A BLOCK -s 46.229.160.0/20 -m comment --comment SEMrushBot -j DROP
+-A BLOCK -s 114.119.160.0/21 -m comment --comment AspiegelBot -j DROP
 
--A RATELIMIT_ENFORCE -m hashlimit --hashlimit-above 4/hour --hashlimit-burst 2 --hashlimit-mode srcip,dstport --hashlimit-name enforce --hashlimit-srcmask 16 -j DROP
+-A RATELIMIT -p tcp -m tcp --dport 22222 -m hashlimit --hashlimit-above 4/hour --hashlimit-burst 2 --hashlimit-mode srcip --hashlimit-name ratelimit-ssh --hashlimit-srcmask 16 -j DROP
+-A RATELIMIT -p tcp -m tcp --dport 25 -m hashlimit --hashlimit-above 4/hour --hashlimit-burst 4 --hashlimit-mode srcip --hashlimit-name ratelimit-smtp --hashlimit-srcmask 16 -j DROP
+-A RATELIMIT -p tcp -m tcp --dport 143 -m hashlimit --hashlimit-above 4/hour --hashlimit-burst 4 --hashlimit-mode srcip --hashlimit-name ratelimit-imap --hashlimit-srcmask 16 -j DROP
+-A RATELIMIT -m hashlimit --hashlimit-above 4/hour --hashlimit-burst 16 --hashlimit-mode srcip,dstport --hashlimit-name ratelimit-other --hashlimit-srcmask 16 -j DROP
 
--A RATELIMIT_REPORT -m hashlimit --hashlimit-above 1/min --hashlimit-burst 6 --hashlimit-mode srcip,dstport --hashlimit-name report1 --hashlimit-srcmask 16 -j LOG --log-prefix "ratelimit report1 " --log-level 5
-
--A RATELIMIT_SUBNET -s 185.0.0.0/8 -j RATELIMIT_ENFORCE
--A RATELIMIT_SUBNET -s 45.0.0.0/8 -j RATELIMIT_ENFORCE
--A RATELIMIT_SUBNET -s 193.0.0.0/8 -j RATELIMIT_ENFORCE
+-A WHITELIST -s 127.0.0.0/8 -m comment --comment localhost -j ACCEPT
+-A WHITELIST -s 10.0.0.0/8 -m comment --comment "RFC 1918" -j ACCEPT
+-A WHITELIST -s 172.16.0.0/12 -m comment --comment "RFC 1918" -j ACCEPT
+-A WHITELIST -s 192.168.0.0/16 -m comment --comment "RFC 1918" -j ACCEPT
 
 COMMIT

From 3200fc17e9fe7588aef60763a1038bf548e86df2 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Thu, 13 Feb 2020 23:01:34 +0100
Subject: [PATCH 28/37] Add nextcloud, backup, and ssh setup

---
 nextcloud/README.md                    | 41 ++++++++++++++++++++++++++
 nextcloud/crontab                      |  2 ++
 nextcloud/fpm.conf                     | 16 ++++++++++
 nextcloud/local.config.php             | 11 +++++++
 nextcloud/nginx.conf                   | 36 ++++++++++++++++++++++
 nextcloud/redis.conf                   |  2 ++
 nginx/README.md                        | 19 ++++++++++++
 nginx/conf.d/ssl.conf                  |  2 ++
 nginx/sites-available/0nohost          |  6 ++++
 nginx/sites-available/redirect-ssl-all |  8 +++++
 nginx/snippets/hsts                    |  1 +
 nginx/snippets/letsencrypt             |  3 ++
 nginx/snippets/redirect-ssl            |  3 ++
 restic/README.md                       | 20 +++++++++++++
 restic/crontab                         |  2 ++
 ssh/README.md                          | 15 ++++++++++
 ssh/sshd_config                        | 13 ++++++++
 17 files changed, 200 insertions(+)
 create mode 100644 nextcloud/README.md
 create mode 100644 nextcloud/crontab
 create mode 100644 nextcloud/fpm.conf
 create mode 100644 nextcloud/local.config.php
 create mode 100644 nextcloud/nginx.conf
 create mode 100644 nextcloud/redis.conf
 create mode 100644 nginx/README.md
 create mode 100644 nginx/conf.d/ssl.conf
 create mode 100644 nginx/sites-available/0nohost
 create mode 100644 nginx/sites-available/redirect-ssl-all
 create mode 100644 nginx/snippets/hsts
 create mode 100644 nginx/snippets/letsencrypt
 create mode 100644 nginx/snippets/redirect-ssl
 create mode 100644 restic/README.md
 create mode 100644 restic/crontab
 create mode 100644 ssh/README.md
 create mode 100644 ssh/sshd_config

diff --git a/nextcloud/README.md b/nextcloud/README.md
new file mode 100644
index 0000000..69888f3
--- /dev/null
+++ b/nextcloud/README.md
@@ -0,0 +1,41 @@
+# Nextcloud
+
+## Create User
+
+```sh
+sudo sed -i '$ a cloud:*:3000:3000::/data/cloud:/bin/sh' /etc/passwd
+sudo sed -i '$ a cloud:x:3000:' /etc/group
+
+sudo mkdir -p /data/cloud
+sudo chown cloud: /data/cloud
+```
+
+## Install Software
+
+```sh
+PHPVERSION=x.x
+
+sudo apt install redis-server nginx php-redis php$PHPVERSION-{cli,curl,fpm,gd,intl,mbstring,sqlite3,xml,zip}
+
+wget https://download.nextcloud.com/server/releases/latest.zip
+sudo -u cloud unzip -d /data/cloud ~/latest.zip
+```
+
+## Apply Configuration
+
+```sh
+DOMAIN=example.com
+
+sudo cp nginx.conf /etc/nginx/sites-available/nextcloud
+sudo cp fpm.conf /etc/php/*/fpm/pool.d/nextcloud.conf
+sudo sed -i '$ r redis.conf' /etc/redis/redis.conf
+
+sudo -u cloud cp local.config.php /data/cloud/nextcloud/config
+sudo -u cloud crontab crontab
+
+sudo ln -s ../sites-available/nextcloud /etc/nginx/sites-enabled
+sudo sed -i s/example.com/$DOMAIN/ /etc/nginx/sites-available/nextcloud /data/cloud/nextcloud/config/local.config.php
+
+sudo -u cloud mkdir -p /data/cloud/.config/user-tmpfiles.d
+echo 'e %h/data/*/files/tmp - - - 7d' | sudo -u cloud tee /data/cloud/.config/user-tmpfiles.d/nextcloud-tmp.conf
+```
diff --git a/nextcloud/crontab b/nextcloud/crontab
new file mode 100644
index 0000000..5775c87
--- /dev/null
+++ b/nextcloud/crontab
@@ -0,0 +1,2 @@
+*/5 *	* * *	php nextcloud/cron.php
+15 3	* * *	systemd-tmpfiles --clean --user
diff --git a/nextcloud/fpm.conf b/nextcloud/fpm.conf
new file mode 100644
index 0000000..0c01b9f
--- /dev/null
+++ b/nextcloud/fpm.conf
@@ -0,0 +1,16 @@
+[cloud]
+
+user = cloud
+group = cloud
+listen = /run/nextcloud.sock
+
+listen.owner = www-data
+listen.group = www-data
+
+env[PATH] = /usr/bin:/bin
+
+pm = ondemand
+pm.max_children = 5
+pm.max_requests = 500
+
+php_admin_value[memory_limit] = 512M
diff --git a/nextcloud/local.config.php b/nextcloud/local.config.php
new file mode 100644
index 0000000..554b3d4
--- /dev/null
+++ b/nextcloud/local.config.php
@@ -0,0 +1,11 @@
+<?php $CONFIG = [
+	'trusted_domains' => [ 'cloud.example.com', '192.168.0.100:8080', 'localhost:8080' ],
+	'versions_retention_obligation' => 'auto, 2',
+	'trashbin_retention_obligation' => 'auto, 2',
+	'memcache.local' => '\OC\Memcache\Redis',
+	'memcache.distributed' => '\OC\Memcache\Redis',
+	'memcache.locking' => '\OC\Memcache\Redis',
+	'redis' => [ 'host' => '/run/redis/redis-server.sock', 'port' => 0 ],
+	'filesystem_check_changes' => 1,
+	'sqlite.journal_mode' => 'WAL',
+];
diff --git a/nextcloud/nginx.conf b/nextcloud/nginx.conf
new file mode 100644
index 0000000..15307c4
--- /dev/null
+++ b/nextcloud/nginx.conf
@@ -0,0 +1,36 @@
+server {
+	server_name cloud.example.com;
+
+	listen 443 ssl;
+
+	set $max_size 4G;
+
+	root /data/cloud/nextcloud;
+	client_max_body_size 0;
+
+	location / {
+		index index.php;
+		try_files $uri /index.php$request_uri;
+	}
+
+	location ~ ^/console\.php {
+		deny all;
+	}
+
+	location ~ ^(.+?\.php)(.*)$ {
+		fastcgi_split_path_info  ^(.+?\.php)(.*)$;
+		fastcgi_pass unix:/run/nextcloud.sock;
+		fastcgi_param PATH_INFO $fastcgi_path_info;
+		fastcgi_param front_controller_active true;
+		fastcgi_param PHP_VALUE "upload_max_filesize=$max_size\n post_max_size=$max_size";
+		include fastcgi.conf;
+	}
+
+	location = /.well-known/carddav {
+		return 301 $scheme://$host:$server_port/remote.php/dav;
+	}
+
+	location = /.well-known/caldav {
+		return 301 $scheme://$host:$server_port/remote.php/dav;
+	}
+}
diff --git a/nextcloud/redis.conf b/nextcloud/redis.conf
new file mode 100644
index 0000000..ed8ff96
--- /dev/null
+++ b/nextcloud/redis.conf
@@ -0,0 +1,2 @@
+unixsocket /run/redis/redis-server.sock
+unixsocketperm 777
diff --git a/nginx/README.md b/nginx/README.md
new file mode 100644
index 0000000..d87501e
--- /dev/null
+++ b/nginx/README.md
@@ -0,0 +1,19 @@
+# Nginx
+
+```sh
+DOMAIN=example.com
+
+sudo cp -r sites-available snippets conf.d /etc/nginx
+
+sudo rm /etc/nginx/sites-*/default
+sudo ln -s ../sites-available/0nohost /etc/nginx/sites-enabled
+sudo ln -s ../sites-available/redirect-ssl-all /etc/nginx/sites-enabled
+
+sudo sed -i s/example.com/$DOMAIN/ /etc/nginx/conf.d/ssl.conf
+```
+
+## Certificate Reload
+
+```sh
+nginx -s reload
+```
diff --git a/nginx/conf.d/ssl.conf b/nginx/conf.d/ssl.conf
new file mode 100644
index 0000000..c68755d
--- /dev/null
+++ b/nginx/conf.d/ssl.conf
@@ -0,0 +1,2 @@
+ssl_certificate /data/ssl/certs/any.example.com/fullchain.pem;
+ssl_certificate_key /data/ssl/certs/any.example.com/privkey.pem;
diff --git a/nginx/sites-available/0nohost b/nginx/sites-available/0nohost
new file mode 100644
index 0000000..e6350ab
--- /dev/null
+++ b/nginx/sites-available/0nohost
@@ -0,0 +1,6 @@
+server {
+	listen 80 default_server;
+	listen 443 ssl default_server;
+	server_name _;
+	return 444;
+}
diff --git a/nginx/sites-available/redirect-ssl-all b/nginx/sites-available/redirect-ssl-all
new file mode 100644
index 0000000..823d16d
--- /dev/null
+++ b/nginx/sites-available/redirect-ssl-all
@@ -0,0 +1,8 @@
+server {
+	server_name .example.com;
+
+	listen 80;
+
+	include snippets/redirect-ssl;
+	include snippets/letsencrypt;
+}
diff --git a/nginx/snippets/hsts b/nginx/snippets/hsts
new file mode 100644
index 0000000..07baaf3
--- /dev/null
+++ b/nginx/snippets/hsts
@@ -0,0 +1 @@
+add_header Strict-Transport-Security max-age=31536000; # 1yr
diff --git a/nginx/snippets/letsencrypt b/nginx/snippets/letsencrypt
new file mode 100644
index 0000000..ac12fe2
--- /dev/null
+++ b/nginx/snippets/letsencrypt
@@ -0,0 +1,3 @@
+location /.well-known/acme-challenge {
+	alias /data/ssl/challenge;
+}
diff --git a/nginx/snippets/redirect-ssl b/nginx/snippets/redirect-ssl
new file mode 100644
index 0000000..385fb49
--- /dev/null
+++ b/nginx/snippets/redirect-ssl
@@ -0,0 +1,3 @@
+location / {
+	return 301 https://$host$request_uri;
+}
diff --git a/restic/README.md b/restic/README.md
new file mode 100644
index 0000000..2f94664
--- /dev/null
+++ b/restic/README.md
@@ -0,0 +1,20 @@
+# Restic backups
+
+Download binary:
+https://github.com/restic/restic/releases/latest
+
+```sh
+REPO=sftp:backup-user@example.com:repo
+
+bunzip2 restic*.bz2
+sudo cp restic* /usr/local/bin/restic
+
+echo 'nice /usr/local/bin/restic -r' "$REPO" '-p /root/backup-key "$@"' | sudo tee /root/restic-cmd
+sudo chmod +x /root/restic-cmd
+
+cat /dev/urandom | base64 | head -c 64 | sudo tee /root/backup-key
+sudo chmod 600 /root/backup-key
+
+sudo /root/restic-cmd init
+sudo crontab crontab
+```
diff --git a/restic/crontab b/restic/crontab
new file mode 100644
index 0000000..5d2bebc
--- /dev/null
+++ b/restic/crontab
@@ -0,0 +1,2 @@
+48 *	* * *	/root/restic-cmd backup -q --exclude-if-present .nobackup /data
+18 3	* * *	/root/restic-cmd forget -q --keep-tag keep -H 24 -d 7 -m 12 -y 100
diff --git a/ssh/README.md b/ssh/README.md
new file mode 100644
index 0000000..a0e5964
--- /dev/null
+++ b/ssh/README.md
@@ -0,0 +1,15 @@
+# SSH
+
+Use only one user `sshlogin` for logins to the server.
+Switch to your main user with `su - adminuser` afterwards.
+
+```sh
+sudo sed -i '$ a sshlogin:x:1001:65534::/home/sshlogin:/bin/sh' /etc/passwd
+sudo sed -i '$ r sshd_config' /etc/ssh/sshd_config
+
+sudo mkdir -p /home/sshlogin/.ssh
+sudo chown sshlogin:root /home/sshlogin/.ssh
+```
+
+* Either create a password with `sudo passwd sshlogin` or
+* Add a key `sudo -u sshlogin editor /home/sshlogin/.ssh/authorized_keys`
diff --git a/ssh/sshd_config b/ssh/sshd_config
new file mode 100644
index 0000000..fb33505
--- /dev/null
+++ b/ssh/sshd_config
@@ -0,0 +1,13 @@
+UseDNS no
+Port 22222
+AllowUsers sshlogin git backup-*
+ClientAliveInterval 10
+
+LoginGraceTime 10
+MaxAuthTries 2
+
+Match User backup-*
+	ForceCommand internal-sftp
+	ChrootDirectory %h
+	X11Forwarding no
+	AllowTcpForwarding no

From 694e6bf290b01d08a00249d2d0f3ea9b862542c6 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Mon, 17 Feb 2020 01:46:24 +0100
Subject: [PATCH 29/37] Add mail setup and named configuration

---
 inbucket/README.md              | 36 ++++++++++++++++++++++++
 inbucket/inbucket.conf          |  9 ++++++
 inbucket/inbucket.service       | 11 ++++++++
 inbucket/nginx.conf             | 20 ++++++++++++++
 mail/README.md                  | 49 +++++++++++++++++++++++++++++++++
 mail/dkim.sql                   |  2 ++
 mail/dovecot/local-sql.conf.ext |  9 ++++++
 mail/dovecot/local.conf         | 23 ++++++++++++++++
 mail/opendkim/local.conf        |  8 ++++++
 mail/postfix/alias.cf           |  2 ++
 mail/postfix/domains.cf         |  2 ++
 mail/postfix/login.cf           |  2 ++
 mail/postfix/main.cf            | 39 ++++++++++++++++++++++++++
 mail/postfix/relay.cf           |  2 ++
 mail/postfix/transport.cf       |  2 ++
 mail/postfix/virtual.cf         |  2 ++
 mail/schema.sql                 |  6 ++++
 named/README.md                 | 19 +++++++++++++
 named/badlist-active.zone       |  8 ++++++
 named/badlist-inactive.zone     |  3 ++
 named/options.conf              |  3 ++
 named/zones.conf                |  1 +
 22 files changed, 258 insertions(+)
 create mode 100644 inbucket/README.md
 create mode 100644 inbucket/inbucket.conf
 create mode 100644 inbucket/inbucket.service
 create mode 100644 inbucket/nginx.conf
 create mode 100644 mail/README.md
 create mode 100644 mail/dkim.sql
 create mode 100644 mail/dovecot/local-sql.conf.ext
 create mode 100644 mail/dovecot/local.conf
 create mode 100644 mail/opendkim/local.conf
 create mode 100644 mail/postfix/alias.cf
 create mode 100644 mail/postfix/domains.cf
 create mode 100644 mail/postfix/login.cf
 create mode 100644 mail/postfix/main.cf
 create mode 100644 mail/postfix/relay.cf
 create mode 100644 mail/postfix/transport.cf
 create mode 100644 mail/postfix/virtual.cf
 create mode 100644 mail/schema.sql
 create mode 100644 named/README.md
 create mode 100644 named/badlist-active.zone
 create mode 100644 named/badlist-inactive.zone
 create mode 100644 named/options.conf
 create mode 100644 named/zones.conf

diff --git a/inbucket/README.md b/inbucket/README.md
new file mode 100644
index 0000000..e190c48
--- /dev/null
+++ b/inbucket/README.md
@@ -0,0 +1,36 @@
+# Inbucket
+
+To be used together with the [`mail`](../mail) setup.
+
+## Install Software
+
+Get: https://www.inbucket.org/binaries/
+
+```sh
+cd /usr/local/share
+sudo tar -xf ~/inbucket_*
+sudo ln -s inbucket_* inbucket
+sudo ln -s ../share/inbucket/inbucket /usr/local/bin
+sudo ln -s ../share/inbucket/inbucket-client /usr/local/bin
+```
+
+## Apply Configuration
+
+```sh
+DOMAIN=test.local
+
+sudo cp inbucket.service /etc/systemd/system
+sudo cp nginx.conf /etc/nginx/sites-available/inbucket
+sudo -u vmail cp inbucket.conf /data/mail/config
+
+sudo ln -s ../sites-available/inbucket /etc/nginx/sites-enabled
+sudo sed -i s/example.com/$DOMAIN/ /etc/nginx/sites-available/inbucket
+
+echo "inbucket:$(openssl passwd -5)" | sudo -u vmail tee /data/mail/config/inbucket.htpasswd
+sudo chown vmail:www-data /data/mail/config/inbucket.htpasswd
+sudo chmod 640 /data/mail/config/inbucket.htpasswd
+
+echo "insert into domain (domain, transport) values ('$DOMAIN','smtp:[localhost]:2500')" | sudo -u vmail sqlite3 /data/mail/config/vmail.db
+
+sudo systemctl enable inbucket
+```
diff --git a/inbucket/inbucket.conf b/inbucket/inbucket.conf
new file mode 100644
index 0000000..8a1817a
--- /dev/null
+++ b/inbucket/inbucket.conf
@@ -0,0 +1,9 @@
+INBUCKET_LOGLEVEL=warn
+INBUCKET_MAILBOXNAMING=full
+INBUCKET_WEB_ADDR=0.0.0.0:8025
+INBUCKET_WEB_UIDIR=/usr/local/share/inbucket/ui
+INBUCKET_WEB_GREETINGFILE=/usr/local/share/inbucket/ui/greeting.html
+INBUCKET_STORAGE_TYPE=file
+INBUCKET_STORAGE_PARAMS=path:/data/mail
+INBUCKET_STORAGE_RETENTIONPERIOD=0
+INBUCKET_STORAGE_MAILBOXMSGCAP=0
diff --git a/inbucket/inbucket.service b/inbucket/inbucket.service
new file mode 100644
index 0000000..5f1d255
--- /dev/null
+++ b/inbucket/inbucket.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Inbucket
+After=network.target
+
+[Service]
+ExecStart=/usr/local/bin/inbucket
+EnvironmentFile=/data/mail/config/inbucket.conf
+User=vmail
+
+[Install]
+WantedBy=multi-user.target
diff --git a/inbucket/nginx.conf b/inbucket/nginx.conf
new file mode 100644
index 0000000..ff52ffd
--- /dev/null
+++ b/inbucket/nginx.conf
@@ -0,0 +1,20 @@
+server {
+	server_name mail.example.com;
+
+	listen 443 ssl;
+
+	auth_basic "Mail";
+	auth_basic_user_file /data/mail/config/inbucket.htpasswd;
+
+	location / {
+		proxy_pass http://localhost:8025;
+		include proxy_params;
+	}
+
+	location /api {
+		proxy_pass http://localhost:8025;
+		include proxy_params;
+		proxy_set_header Upgrade $http_upgrade;
+		proxy_set_header Connection "Upgrade";
+	}
+}
diff --git a/mail/README.md b/mail/README.md
new file mode 100644
index 0000000..1119549
--- /dev/null
+++ b/mail/README.md
@@ -0,0 +1,49 @@
+# Mail with SQLite
+
+## Create User
+
+```sh
+sudo sed -i '$ a vmail:*:2000:2000::/data/mail:/usr/sbin/nologin' /etc/passwd
+sudo sed -i '$ a vmail:x:2000:' /etc/group
+
+sudo mkdir -p /data/mail/mail
+sudo mkdir -p /data/mail/config
+sudo chown vmail: /data/mail/*
+
+cat schema.sql | sudo -u vmail sqlite3 /data/mail/config/vmail.db
+sudo chmod 640 /data/mail/config/vmail.db
+```
+
+## Install Software
+
+```sh
+sudo apt install sqlite3 postfix postfix-sqlite dovecot-imapd dovecot-sqlite opendkim libopendbx1-sqlite3
+```
+
+## Apply Configuration
+
+```sh
+DOMAIN=example.com
+
+sudo cp -r postfix dovecot /etc
+sudo sed -i '$ r opendkim/local.conf' /etc/opendkim.conf
+sudo sed -i s/example.com/$DOMAIN/ /etc/postfix/main.cf /etc/dovecot/local.conf
+sudo sed -i '/include auth-system/ s/.*/#&/' /etc/dovecot/conf.d/10-auth.conf
+
+sudo ln -s /data/mail/config/vmail.db /.opendkim-bug-241.db
+sudo chown vmail:postfix /data/mail/config/vmail.db
+
+opendkim-genkey -d $DOMAIN -s s
+chmod +r s.private
+cat dkim.sql | sed s/DOMAIN/$DOMAIN/ | sudo -u vmail sqlite3 /data/mail/config/vmail.db
+cat s.txt
+rm s.private s.txt
+```
+
+## Notes
+
+* The `vmail.db` parent directory needs to be writeable by the user modifying the database
+* The postfix process does not load the supplementary groups (`set_eugid` only sets one gid),
+  hence the vmail database needs to be readable by the postfix primary group
+* The dovecot process runs as root and can access the database
+* OpenDKIM's `dsn` parsing is broken and opens the database in the root directory
diff --git a/mail/dkim.sql b/mail/dkim.sql
new file mode 100644
index 0000000..c0e58e8
--- /dev/null
+++ b/mail/dkim.sql
@@ -0,0 +1,2 @@
+insert into dkim (match, key) values ("DOMAIN", "default");
+insert into dkim_key (key, domain, selector, private_key) values ("default", "DOMAIN", "s", readfile("s.private"));
diff --git a/mail/dovecot/local-sql.conf.ext b/mail/dovecot/local-sql.conf.ext
new file mode 100644
index 0000000..ee22dec
--- /dev/null
+++ b/mail/dovecot/local-sql.conf.ext
@@ -0,0 +1,9 @@
+driver = sqlite
+connect = /data/mail/config/vmail.db
+
+user_query = SELECT \
+ REPLACE('/data/mail/mail/{dir}', '{dir}', maildir) AS home \
+ , REPLACE('maildir:/data/mail/mail/{dir}:LAYOUT=fs', '{dir}', maildir) AS mail \
+ FROM mailbox WHERE username = '%u' AND active = 1
+
+password_query = SELECT password FROM mailbox WHERE username = '%u'
diff --git a/mail/dovecot/local.conf b/mail/dovecot/local.conf
new file mode 100644
index 0000000..fd9ce3e
--- /dev/null
+++ b/mail/dovecot/local.conf
@@ -0,0 +1,23 @@
+protocols = imap
+mail_uid = vmail
+mail_gid = vmail
+
+ssl = required
+ssl_key = </data/ssl/certs/mail.example.com/privkey.pem
+ssl_cert = </data/ssl/certs/mail.example.com/fullchain.pem
+
+userdb {
+	driver = sql
+	args = /etc/dovecot/local-sql.conf.ext
+}
+
+passdb {
+	driver = sql
+	args = /etc/dovecot/local-sql.conf.ext
+}
+
+service auth {
+	unix_listener /var/spool/postfix/private/auth {
+		user = postfix
+	}
+}
diff --git a/mail/opendkim/local.conf b/mail/opendkim/local.conf
new file mode 100644
index 0000000..72a9dc7
--- /dev/null
+++ b/mail/opendkim/local.conf
@@ -0,0 +1,8 @@
+Socket local:/var/spool/postfix/private/opendkim
+UserID postfix
+InternalHosts 0.0.0.0/0
+
+Canonicalization relaxed/relaxed
+
+KeyTable dsn:sqlite3://./opendkim-bug-241.db/table=dkim_key?keycol=key?datacol=domain,selector,private_key
+SigningTable dsn:sqlite3://./opendkim-bug-241.db/table=dkim?keycol=match?datacol=key
diff --git a/mail/postfix/alias.cf b/mail/postfix/alias.cf
new file mode 100644
index 0000000..856c3cc
--- /dev/null
+++ b/mail/postfix/alias.cf
@@ -0,0 +1,2 @@
+dbpath = /data/mail/config/vmail.db
+query = SELECT goto FROM alias WHERE address = '%s' AND active = 1
diff --git a/mail/postfix/domains.cf b/mail/postfix/domains.cf
new file mode 100644
index 0000000..619b14a
--- /dev/null
+++ b/mail/postfix/domains.cf
@@ -0,0 +1,2 @@
+dbpath = /data/mail/config/vmail.db
+query = SELECT domain FROM domain WHERE domain = '%s' AND active = 1 AND NOT (transport LIKE 'smtp%%' OR transport LIKE 'relay%%')
diff --git a/mail/postfix/login.cf b/mail/postfix/login.cf
new file mode 100644
index 0000000..30acf06
--- /dev/null
+++ b/mail/postfix/login.cf
@@ -0,0 +1,2 @@
+dbpath = /data/mail/config/vmail.db
+query = SELECT username FROM mailbox WHERE username = '%s' AND active = 1
diff --git a/mail/postfix/main.cf b/mail/postfix/main.cf
new file mode 100644
index 0000000..4b2e97f
--- /dev/null
+++ b/mail/postfix/main.cf
@@ -0,0 +1,39 @@
+# System
+
+biff = no
+compatibility_level = 2
+disable_vrfy_command = yes
+mailbox_size_limit = 0
+message_size_limit = 0
+mydomain = local
+mynetworks_style = subnet
+
+# TLS
+
+smtp_tls_security_level = may
+smtpd_tls_security_level = may
+smtpd_tls_key_file =  /data/ssl/certs/mail.example.com/privkey.pem
+smtpd_tls_cert_file = /data/ssl/certs/mail.example.com/fullchain.pem
+
+# Custom
+
+relay_domains = sqlite:/etc/postfix/relay.cf
+transport_maps = sqlite:/etc/postfix/transport.cf
+
+recipient_delimiter = +
+virtual_mailbox_base = /data/mail/mail
+virtual_uid_maps = static:2000
+virtual_gid_maps = static:2000
+virtual_mailbox_domains = sqlite:/etc/postfix/domains.cf
+virtual_mailbox_maps = sqlite:/etc/postfix/virtual.cf
+virtual_alias_maps = sqlite:/etc/postfix/alias.cf
+virtual_mailbox_limit = 0
+
+smtpd_sasl_auth_enable = yes
+smtpd_sasl_type = dovecot
+smtpd_sasl_path = private/auth
+smtpd_sender_restrictions = reject_sender_login_mismatch
+smtpd_sender_login_maps = sqlite:/etc/postfix/login.cf, $virtual_alias_maps
+
+smtpd_milters = unix:private/opendkim
+non_smtpd_milters = $smtpd_milters
diff --git a/mail/postfix/relay.cf b/mail/postfix/relay.cf
new file mode 100644
index 0000000..7fe86eb
--- /dev/null
+++ b/mail/postfix/relay.cf
@@ -0,0 +1,2 @@
+dbpath = /data/mail/config/vmail.db
+query = SELECT domain FROM domain WHERE domain = '%s' AND active = 1 AND (transport LIKE 'smtp%%' OR transport LIKE 'relay%%')
diff --git a/mail/postfix/transport.cf b/mail/postfix/transport.cf
new file mode 100644
index 0000000..0e8deac
--- /dev/null
+++ b/mail/postfix/transport.cf
@@ -0,0 +1,2 @@
+dbpath = /data/mail/config/vmail.db
+query = SELECT transport FROM domain WHERE domain = '%s' AND active = 1
diff --git a/mail/postfix/virtual.cf b/mail/postfix/virtual.cf
new file mode 100644
index 0000000..dd3705d
--- /dev/null
+++ b/mail/postfix/virtual.cf
@@ -0,0 +1,2 @@
+dbpath = /data/mail/config/vmail.db
+query = SELECT maildir FROM mailbox WHERE username = '%s' AND active = 1
diff --git a/mail/schema.sql b/mail/schema.sql
new file mode 100644
index 0000000..7bc9222
--- /dev/null
+++ b/mail/schema.sql
@@ -0,0 +1,6 @@
+create table alias (address varchar(255) not null primary key, goto varchar(255) not null, active int not null default 1);
+create table domain (domain varchar(255) not null primary key, transport varchar(255) not null default 'virtual', active int not null default 1);
+create table mailbox (username varchar(255) not null primary key, password varchar(255) not null default '', name varchar(255) not null default '', maildir varchar(255) not null, active int not null default 1);
+
+create table dkim (match varchar(255) not null primary key, key varchar(255) not null);
+create table dkim_key (key varchar(255) not null primary key, domain varchar(255) not null, selector varchar(255) not null, private_key varchar(65535) not null);
diff --git a/named/README.md b/named/README.md
new file mode 100644
index 0000000..520daa7
--- /dev/null
+++ b/named/README.md
@@ -0,0 +1,19 @@
+# Named
+
+## Install Software
+
+```sh
+sudo apt install bind9
+```
+
+## Apply Configuration
+
+```sh
+sudo mkdir /data/named
+sudo cp *.conf *.zone /data/named
+
+sudo sed -i '/directory/ a \\tinclude "/data/named/options.conf";' /etc/bind/named.conf.options
+sudo sed -i '$ a include "/data/named/zones.conf";' /etc/bind/named.conf
+
+echo '/data/named/** r,' | sudo tee -a /etc/apparmor.d/local/usr.sbin.named
+```
diff --git a/named/badlist-active.zone b/named/badlist-active.zone
new file mode 100644
index 0000000..ccd42f4
--- /dev/null
+++ b/named/badlist-active.zone
@@ -0,0 +1,8 @@
+$TTL 1H
+@	SOA	localhost. named-mgr.example.com (1 1W 1W 1W 1H)
+@	NS	localhost.
+
+facebook.com	CNAME	.
+*.facebook.com	CNAME	.
+instagram.com	CNAME	.
+*.instagram.com	CNAME	.
diff --git a/named/badlist-inactive.zone b/named/badlist-inactive.zone
new file mode 100644
index 0000000..e6bd4d3
--- /dev/null
+++ b/named/badlist-inactive.zone
@@ -0,0 +1,3 @@
+$TTL 1H
+@	SOA	localhost. named-mgr.example.com (1 1W 1W 1W 1H)
+@	NS	localhost.
diff --git a/named/options.conf b/named/options.conf
new file mode 100644
index 0000000..33723f3
--- /dev/null
+++ b/named/options.conf
@@ -0,0 +1,3 @@
+forward only;
+forwarders { 1.1.1.1; };
+response-policy { zone "badlist"; };
diff --git a/named/zones.conf b/named/zones.conf
new file mode 100644
index 0000000..b2cedf0
--- /dev/null
+++ b/named/zones.conf
@@ -0,0 +1 @@
+zone "badlist" { type master; file "/data/named/badlist-active.zone"; };

From 14a6bcc003e89a81b488780ed3825062df70eb4e Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sat, 22 Feb 2020 14:20:30 +0100
Subject: [PATCH 30/37] Add NetworkManager configuration

---
 network-manager/99-no-wifi-on-ethernet | 11 +++++++++++
 network-manager/README.md              | 13 +++++++++++++
 2 files changed, 24 insertions(+)
 create mode 100755 network-manager/99-no-wifi-on-ethernet
 create mode 100644 network-manager/README.md

diff --git a/network-manager/99-no-wifi-on-ethernet b/network-manager/99-no-wifi-on-ethernet
new file mode 100755
index 0000000..fa44a2f
--- /dev/null
+++ b/network-manager/99-no-wifi-on-ethernet
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+logger -t no-wifi-on-ethernet "Device $1 is $2"
+
+if [ "dev:$1:$2" = "dev:eth0:up" ]; then
+	nmcli r wifi off
+fi
+
+if [ "dev:$1:$2" = "dev:eth0:down" ]; then
+	nmcli r wifi on
+fi
diff --git a/network-manager/README.md b/network-manager/README.md
new file mode 100644
index 0000000..6e60a35
--- /dev/null
+++ b/network-manager/README.md
@@ -0,0 +1,13 @@
+# NetworkManager
+
+## Manage ethernet devices with NetworkManager
+
+```sh
+touch /etc/NetworkManager/conf.d/10-globally-managed-devices.conf
+```
+
+## Automatically switch off wifi when ethernet is connected
+
+```sh
+sudo cp 99-no-wifi-on-ethernet /etc/NetworkManager/dispatcher.d
+```

From 7ad59d276e25045175b10fb55d671816b1156918 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Wed, 26 Feb 2020 01:14:32 +0100
Subject: [PATCH 31/37] Add iptables, gitea, and iodine services

---
 gitea/README.md           | 43 +++++++++++++++++++++++++++++++++++++++
 gitea/gitea.service       | 10 +++++++++
 gitea/nginx.conf          | 10 +++++++++
 iodine/README.md          | 36 ++++++++++++++++++++++++++++++++
 iodine/my-iodined.conf    |  2 ++
 iodine/my-iodined.service | 11 ++++++++++
 iptables/README.md        | 33 ++++++++++++++++++++++++++++++
 iptables/default.rules    | 15 ++++++++++++++
 iptables/empty.rules      | 11 ++++++++++
 iptables/iptables.service | 13 ++++++++++++
 iptables/ratelimit.rules  | 16 +++++++++++++++
 11 files changed, 200 insertions(+)
 create mode 100644 gitea/README.md
 create mode 100644 gitea/gitea.service
 create mode 100644 gitea/nginx.conf
 create mode 100644 iodine/README.md
 create mode 100644 iodine/my-iodined.conf
 create mode 100644 iodine/my-iodined.service
 create mode 100644 iptables/README.md
 create mode 100644 iptables/default.rules
 create mode 100644 iptables/empty.rules
 create mode 100644 iptables/iptables.service
 create mode 100644 iptables/ratelimit.rules

diff --git a/gitea/README.md b/gitea/README.md
new file mode 100644
index 0000000..6fbb691
--- /dev/null
+++ b/gitea/README.md
@@ -0,0 +1,43 @@
+# Gitea
+
+## Create User
+
+```sh
+sudo sed -i '$ a git:*:2050:2050::/data/git:/bin/sh' /etc/passwd
+sudo sed -i '$ a git:x:2050:' /etc/group
+
+sudo mkdir -p /data/git/gitea
+sudo chown -R git: /data/git
+```
+
+## Install Software
+
+```sh
+sudo apt install git
+sudo -u git wget -O /data/git/gitea/gitea https://dl.gitea.io/gitea/...
+sudo -u git chmod +x /data/git/gitea/gitea
+```
+
+## Apply Configuration
+
+```sh
+DOMAIN=example.com
+
+sudo cp nginx.conf /etc/nginx/sites-available/git
+sudo cp gitea.service /etc/systemd/system
+
+sudo systemctl enable gitea
+sudo systemctl start gitea
+
+sudo sed -i s/example.com/$DOMAIN/ /etc/nginx/sites-available/git
+
+sudo ln -s ../sites-available/git /etc/nginx/sites-enabled
+sudo nginx -s reload
+
+
+sudo iptables -t nat -A PREROUTING -p tcp --dport 4986 -j REDIRECT --to-ports 22222
+
+sudo -u git sed -i /MODE.*file/s/file/console/ /data/git/gitea/custom/conf/app.ini
+sudo -u git sed -i /LEVEL.*info/s/info/warn/ /data/git/gitea/custom/conf/app.ini
+sudo -u git /data/git/gitea/gitea admin create-user --username admin --password admin --admin --email admin
+```
diff --git a/gitea/gitea.service b/gitea/gitea.service
new file mode 100644
index 0000000..4bc4533
--- /dev/null
+++ b/gitea/gitea.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=Gitea
+After=network.target
+
+[Service]
+ExecStart=/data/git/gitea/gitea web
+User=git
+
+[Install]
+WantedBy=multi-user.target
diff --git a/gitea/nginx.conf b/gitea/nginx.conf
new file mode 100644
index 0000000..b0af6b5
--- /dev/null
+++ b/gitea/nginx.conf
@@ -0,0 +1,10 @@
+server {
+	server_name git.example.com;
+
+	listen 443 ssl;
+
+	location / {
+		proxy_pass http://localhost:3000;
+		include proxy_params;
+	}
+}
diff --git a/iodine/README.md b/iodine/README.md
new file mode 100644
index 0000000..3c2d6bb
--- /dev/null
+++ b/iodine/README.md
@@ -0,0 +1,36 @@
+# Iodine
+
+## Install Software
+
+```sh
+sudo apt install iodine
+```
+
+## Apply Configuration
+
+```sh
+EXTERNAL=eth0
+INTERNAL=dns0
+
+echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
+echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/60-ipv4-forward.conf
+
+sudo iptables -t nat -A POSTROUTING -o $EXTERNAL -j MASQUERADE
+sudo iptables -A INPUT -p udp --dport 5353 -j ACCEPT
+sudo iptables -A INPUT -i $INTERNAL -j ACCEPT
+
+# Necessary only if default policy is not ACCEPT
+sudo iptables -A FORWARD -i $EXTERNAL -o $INTERNAL -m state --state RELATED,ESTABLISHED -j ACCEPT
+sudo iptables -A FORWARD -i $INTERNAL -o $EXTERNAL -j ACCEPT
+
+# Adjust domain:
+sudo iptables -t nat -A PREROUTING -p udp --dport 53 -m string --hex-string "|01|t|07|example|03|com|00|" --algo bm --from 20 --to 65535 -j REDIRECT --to-ports 5353
+
+sudo cp my-iodined.service /etc/systemd/system
+sudo cp my-iodined.conf /etc
+sudo chmod 600 /etc/my-iodined.conf
+
+sudo editor /etc/my-iodined.conf
+sudo systemctl enable my-iodined
+sudo systemctl start my-iodined
+```
diff --git a/iodine/my-iodined.conf b/iodine/my-iodined.conf
new file mode 100644
index 0000000..d454d38
--- /dev/null
+++ b/iodine/my-iodined.conf
@@ -0,0 +1,2 @@
+IODINED_OPTS="-c -p 5353 192.168.100.1 t.example.com"
+IODINED_PASS=secret
diff --git a/iodine/my-iodined.service b/iodine/my-iodined.service
new file mode 100644
index 0000000..953abfc
--- /dev/null
+++ b/iodine/my-iodined.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Iodine
+After=network-online.target
+
+[Service]
+EnvironmentFile=/etc/my-iodined.conf
+ExecStartPre=mkdir -p /run/iodined
+ExecStart=iodined -f -u nobody -t /run/iodined $IODINED_OPTS
+
+[Install]
+WantedBy=multi-user.target
diff --git a/iptables/README.md b/iptables/README.md
new file mode 100644
index 0000000..3e2a65d
--- /dev/null
+++ b/iptables/README.md
@@ -0,0 +1,33 @@
+# Iptables
+
+```sh
+sudo cp *.rules /etc
+sudo cp *.service /etc/systemd/system
+sudo systemctl enable iptables
+```
+
+## Apply and Report Rate Limits
+
+The `ratelimit.rules` file adds new chains to rate limit subnets.
+
+```sh
+sudo iptables-restore -n < ratelimit.rules
+
+# Common offenders
+sudo iptables -t raw -A RATELIMIT_SUBNET -s 185.0.0.0/8 -j RATELIMIT_DEFAULT
+sudo iptables -t raw -A RATELIMIT_SUBNET -s 45.0.0.0/8 -j RATELIMIT_DEFAULT
+sudo iptables -t raw -A RATELIMIT_SUBNET -s 193.0.0.0/8 -j RATELIMIT_DEFAULT
+
+# Default action
+sudo iptables -t raw -A RATELIMIT_DEFAULT -p tcp --tcp-flags SYN,ACK SYN \
+ -m hashlimit --hashlimit-name drop_4h \
+ --hashlimit-above 4/hour --hashlimit-burst 2 \
+ --hashlimit-mode srcip,dstport --hashlimit-srcmask 16 -j DROP
+
+# Log potential offenders
+sudo iptables -t raw -A RATELIMIT_REPORT -p tcp --tcp-flags SYN,ACK SYN \
+ -m hashlimit --hashlimit-name report1 \
+ --hashlimit-above 1/second --hashlimit-burst 4 \
+ --hashlimit-mode srcip,dstport --hashlimit-srcmask 16 -j LOG \
+ --log-level 5 --log-prefix "ratelimit report1 "
+```
diff --git a/iptables/default.rules b/iptables/default.rules
new file mode 100644
index 0000000..f6c250b
--- /dev/null
+++ b/iptables/default.rules
@@ -0,0 +1,15 @@
+*filter
+:INPUT DROP
+:CONNECTION -
+:SERVICE -
+-A INPUT -j CONNECTION
+-A INPUT -j SERVICE
+-A CONNECTION -i lo -j ACCEPT
+-A CONNECTION -m state --state RELATED,ESTABLISHED -j ACCEPT
+-A SERVICE -p tcp --dport 25 -j ACCEPT
+-A SERVICE -p tcp --dport 80 -j ACCEPT
+-A SERVICE -p tcp --dport 143 -j ACCEPT
+-A SERVICE -p tcp --dport 443 -j ACCEPT
+-A SERVICE -p tcp --dport 22222 -j ACCEPT
+-A SERVICE -p udp --dport 53 -j ACCEPT
+COMMIT
diff --git a/iptables/empty.rules b/iptables/empty.rules
new file mode 100644
index 0000000..7a7d32c
--- /dev/null
+++ b/iptables/empty.rules
@@ -0,0 +1,11 @@
+*filter
+:INPUT ACCEPT
+COMMIT
+*nat
+COMMIT
+*mangle
+COMMIT
+*raw
+COMMIT
+*security
+COMMIT
diff --git a/iptables/iptables.service b/iptables/iptables.service
new file mode 100644
index 0000000..45847f3
--- /dev/null
+++ b/iptables/iptables.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=iptables
+Before=network-pre.target
+Wants=network-pre.target
+
+[Service]
+Type=oneshot
+ExecStart=iptables-restore /etc/default.rules
+ExecStop=iptables-restore /etc/empty.rules
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
diff --git a/iptables/ratelimit.rules b/iptables/ratelimit.rules
new file mode 100644
index 0000000..e7dfdc9
--- /dev/null
+++ b/iptables/ratelimit.rules
@@ -0,0 +1,16 @@
+*raw
+:RATELIMIT -
+:RATELIMIT_REPORT -
+:RATELIMIT_SUBNET -
+:RATELIMIT_DEFAULT -
+
+-I PREROUTING -j RATELIMIT
+
+-A RATELIMIT -s 127.0.0.0/8 -j RETURN
+-A RATELIMIT -s 10.0.0.0/8 -j RETURN
+-A RATELIMIT -s 172.16.0.0/12 -j RETURN
+-A RATELIMIT -s 192.168.0.0/16 -j RETURN
+-A RATELIMIT -j RATELIMIT_SUBNET
+-A RATELIMIT -j RATELIMIT_REPORT
+
+COMMIT

From e60d9c0c59d2dc532dec89c785cf6dbb6451fe91 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sat, 7 Mar 2020 09:24:38 +0100
Subject: [PATCH 32/37] Add robots exception for gitea

---
 gitea/nginx.conf | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/gitea/nginx.conf b/gitea/nginx.conf
index b0af6b5..56a0b41 100644
--- a/gitea/nginx.conf
+++ b/gitea/nginx.conf
@@ -7,4 +7,6 @@ server {
 		proxy_pass http://localhost:3000;
 		include proxy_params;
 	}
+
+	location = /robots.txt { return 200 "User-agent: *\nDisallow: */commit/*\n"; }
 }

From 6f80b8345d5d3acd0dea01c4470e26a92e21bb80 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Sat, 7 Mar 2020 09:24:38 +0100
Subject: [PATCH 33/37] Update iptables

---
 iptables/README.md       | 21 ++-------------------
 iptables/ratelimit.rules | 12 ++++++++++--
 2 files changed, 12 insertions(+), 21 deletions(-)

diff --git a/iptables/README.md b/iptables/README.md
index 3e2a65d..fc67e5c 100644
--- a/iptables/README.md
+++ b/iptables/README.md
@@ -8,26 +8,9 @@ sudo systemctl enable iptables
 
 ## Apply and Report Rate Limits
 
-The `ratelimit.rules` file adds new chains to rate limit subnets.
+The `ratelimit.rules` file adds new chains to
+limit the rate of new connections based on /16 subnets.
 
 ```sh
 sudo iptables-restore -n < ratelimit.rules
-
-# Common offenders
-sudo iptables -t raw -A RATELIMIT_SUBNET -s 185.0.0.0/8 -j RATELIMIT_DEFAULT
-sudo iptables -t raw -A RATELIMIT_SUBNET -s 45.0.0.0/8 -j RATELIMIT_DEFAULT
-sudo iptables -t raw -A RATELIMIT_SUBNET -s 193.0.0.0/8 -j RATELIMIT_DEFAULT
-
-# Default action
-sudo iptables -t raw -A RATELIMIT_DEFAULT -p tcp --tcp-flags SYN,ACK SYN \
- -m hashlimit --hashlimit-name drop_4h \
- --hashlimit-above 4/hour --hashlimit-burst 2 \
- --hashlimit-mode srcip,dstport --hashlimit-srcmask 16 -j DROP
-
-# Log potential offenders
-sudo iptables -t raw -A RATELIMIT_REPORT -p tcp --tcp-flags SYN,ACK SYN \
- -m hashlimit --hashlimit-name report1 \
- --hashlimit-above 1/second --hashlimit-burst 4 \
- --hashlimit-mode srcip,dstport --hashlimit-srcmask 16 -j LOG \
- --log-level 5 --log-prefix "ratelimit report1 "
 ```
diff --git a/iptables/ratelimit.rules b/iptables/ratelimit.rules
index e7dfdc9..db4319f 100644
--- a/iptables/ratelimit.rules
+++ b/iptables/ratelimit.rules
@@ -1,10 +1,10 @@
 *raw
 :RATELIMIT -
+:RATELIMIT_ENFORCE -
 :RATELIMIT_REPORT -
 :RATELIMIT_SUBNET -
-:RATELIMIT_DEFAULT -
 
--I PREROUTING -j RATELIMIT
+-I PREROUTING -p tcp --tcp-flags SYN,ACK SYN -j RATELIMIT
 
 -A RATELIMIT -s 127.0.0.0/8 -j RETURN
 -A RATELIMIT -s 10.0.0.0/8 -j RETURN
@@ -13,4 +13,12 @@
 -A RATELIMIT -j RATELIMIT_SUBNET
 -A RATELIMIT -j RATELIMIT_REPORT
 
+-A RATELIMIT_ENFORCE -m hashlimit --hashlimit-above 4/hour --hashlimit-burst 2 --hashlimit-mode srcip,dstport --hashlimit-name enforce --hashlimit-srcmask 16 -j DROP
+
+-A RATELIMIT_REPORT -m hashlimit --hashlimit-above 1/min --hashlimit-burst 6 --hashlimit-mode srcip,dstport --hashlimit-name report1 --hashlimit-srcmask 16 -j LOG --log-prefix "ratelimit report1 " --log-level 5
+
+-A RATELIMIT_SUBNET -s 185.0.0.0/8 -j RATELIMIT_ENFORCE
+-A RATELIMIT_SUBNET -s 45.0.0.0/8 -j RATELIMIT_ENFORCE
+-A RATELIMIT_SUBNET -s 193.0.0.0/8 -j RATELIMIT_ENFORCE
+
 COMMIT

From adde853701aec0d11e171327f6f25f83a791b6dd Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Mon, 13 Apr 2020 21:10:11 +0200
Subject: [PATCH 34/37] Add postfix SNI support

---
 mail/README.md       | 12 ++++++++++--
 mail/postfix/main.cf |  1 +
 mail/postfix/sni.cf  |  1 +
 3 files changed, 12 insertions(+), 2 deletions(-)
 create mode 100644 mail/postfix/sni.cf

diff --git a/mail/README.md b/mail/README.md
index 1119549..7945019 100644
--- a/mail/README.md
+++ b/mail/README.md
@@ -11,6 +11,7 @@ sudo mkdir -p /data/mail/config
 sudo chown vmail: /data/mail/*
 
 cat schema.sql | sudo -u vmail sqlite3 /data/mail/config/vmail.db
+sudo chown vmail:postfix /data/mail/config/vmail.db
 sudo chmod 640 /data/mail/config/vmail.db
 ```
 
@@ -26,12 +27,13 @@ sudo apt install sqlite3 postfix postfix-sqlite dovecot-imapd dovecot-sqlite ope
 DOMAIN=example.com
 
 sudo cp -r postfix dovecot /etc
+sudo chmod 600 /etc/postfix/sni.cf
+
 sudo sed -i '$ r opendkim/local.conf' /etc/opendkim.conf
-sudo sed -i s/example.com/$DOMAIN/ /etc/postfix/main.cf /etc/dovecot/local.conf
+sudo sed -i s/example.com/$DOMAIN/ /etc/postfix/{main,sni}.cf /etc/dovecot/local.conf
 sudo sed -i '/include auth-system/ s/.*/#&/' /etc/dovecot/conf.d/10-auth.conf
 
 sudo ln -s /data/mail/config/vmail.db /.opendkim-bug-241.db
-sudo chown vmail:postfix /data/mail/config/vmail.db
 
 opendkim-genkey -d $DOMAIN -s s
 chmod +r s.private
@@ -40,6 +42,12 @@ cat s.txt
 rm s.private s.txt
 ```
 
+## Certificate Reload
+
+```sh
+postmap -F /etc/postfix/sni.cf
+```
+
 ## Notes
 
 * The `vmail.db` parent directory needs to be writeable by the user modifying the database
diff --git a/mail/postfix/main.cf b/mail/postfix/main.cf
index 4b2e97f..66b0ba0 100644
--- a/mail/postfix/main.cf
+++ b/mail/postfix/main.cf
@@ -14,6 +14,7 @@ smtp_tls_security_level = may
 smtpd_tls_security_level = may
 smtpd_tls_key_file =  /data/ssl/certs/mail.example.com/privkey.pem
 smtpd_tls_cert_file = /data/ssl/certs/mail.example.com/fullchain.pem
+tls_server_sni_maps = hash:/etc/postfix/sni.cf
 
 # Custom
 
diff --git a/mail/postfix/sni.cf b/mail/postfix/sni.cf
new file mode 100644
index 0000000..e9ae8a0
--- /dev/null
+++ b/mail/postfix/sni.cf
@@ -0,0 +1 @@
+mail.example.com /data/ssl/certs/mail.example.com/privkey.pem /data/ssl/certs/mail.example.com/fullchain.pem

From f5c807633bd32e0824e506fee051b52accf96c46 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Tue, 14 Apr 2020 00:43:49 +0200
Subject: [PATCH 35/37] Improve iptables ratelimit rules

---
 iptables/ratelimit.rules | 31 +++++++++++++++----------------
 1 file changed, 15 insertions(+), 16 deletions(-)

diff --git a/iptables/ratelimit.rules b/iptables/ratelimit.rules
index db4319f..c5dc31a 100644
--- a/iptables/ratelimit.rules
+++ b/iptables/ratelimit.rules
@@ -1,24 +1,23 @@
 *raw
+:BLOCK -
 :RATELIMIT -
-:RATELIMIT_ENFORCE -
-:RATELIMIT_REPORT -
-:RATELIMIT_SUBNET -
+:WHITELIST -
 
--I PREROUTING -p tcp --tcp-flags SYN,ACK SYN -j RATELIMIT
+-A PREROUTING -j WHITELIST
+-A PREROUTING -j BLOCK
+-A PREROUTING -p tcp -m tcp --tcp-flags SYN,ACK SYN -j RATELIMIT
 
--A RATELIMIT -s 127.0.0.0/8 -j RETURN
--A RATELIMIT -s 10.0.0.0/8 -j RETURN
--A RATELIMIT -s 172.16.0.0/12 -j RETURN
--A RATELIMIT -s 192.168.0.0/16 -j RETURN
--A RATELIMIT -j RATELIMIT_SUBNET
--A RATELIMIT -j RATELIMIT_REPORT
+-A BLOCK -s 46.229.160.0/20 -m comment --comment SEMrushBot -j DROP
+-A BLOCK -s 114.119.160.0/21 -m comment --comment AspiegelBot -j DROP
 
--A RATELIMIT_ENFORCE -m hashlimit --hashlimit-above 4/hour --hashlimit-burst 2 --hashlimit-mode srcip,dstport --hashlimit-name enforce --hashlimit-srcmask 16 -j DROP
+-A RATELIMIT -p tcp -m tcp --dport 22222 -m hashlimit --hashlimit-above 4/hour --hashlimit-burst 2 --hashlimit-mode srcip --hashlimit-name ratelimit-ssh --hashlimit-srcmask 16 -j DROP
+-A RATELIMIT -p tcp -m tcp --dport 25 -m hashlimit --hashlimit-above 4/hour --hashlimit-burst 4 --hashlimit-mode srcip --hashlimit-name ratelimit-smtp --hashlimit-srcmask 16 -j DROP
+-A RATELIMIT -p tcp -m tcp --dport 143 -m hashlimit --hashlimit-above 4/hour --hashlimit-burst 4 --hashlimit-mode srcip --hashlimit-name ratelimit-imap --hashlimit-srcmask 16 -j DROP
+-A RATELIMIT -m hashlimit --hashlimit-above 4/hour --hashlimit-burst 16 --hashlimit-mode srcip,dstport --hashlimit-name ratelimit-other --hashlimit-srcmask 16 -j DROP
 
--A RATELIMIT_REPORT -m hashlimit --hashlimit-above 1/min --hashlimit-burst 6 --hashlimit-mode srcip,dstport --hashlimit-name report1 --hashlimit-srcmask 16 -j LOG --log-prefix "ratelimit report1 " --log-level 5
-
--A RATELIMIT_SUBNET -s 185.0.0.0/8 -j RATELIMIT_ENFORCE
--A RATELIMIT_SUBNET -s 45.0.0.0/8 -j RATELIMIT_ENFORCE
--A RATELIMIT_SUBNET -s 193.0.0.0/8 -j RATELIMIT_ENFORCE
+-A WHITELIST -s 127.0.0.0/8 -m comment --comment localhost -j ACCEPT
+-A WHITELIST -s 10.0.0.0/8 -m comment --comment "RFC 1918" -j ACCEPT
+-A WHITELIST -s 172.16.0.0/12 -m comment --comment "RFC 1918" -j ACCEPT
+-A WHITELIST -s 192.168.0.0/16 -m comment --comment "RFC 1918" -j ACCEPT
 
 COMMIT

From 06d05bc46fec31c611e7e64ba61efaf3e7d72943 Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Wed, 15 Apr 2020 23:22:14 +0200
Subject: [PATCH 36/37] Fix unprocessed index directives

---
 nextcloud/nginx.conf | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/nextcloud/nginx.conf b/nextcloud/nginx.conf
index 15307c4..8030b02 100644
--- a/nextcloud/nginx.conf
+++ b/nextcloud/nginx.conf
@@ -9,7 +9,6 @@ server {
 	client_max_body_size 0;
 
 	location / {
-		index index.php;
 		try_files $uri /index.php$request_uri;
 	}
 
@@ -26,6 +25,10 @@ server {
 		include fastcgi.conf;
 	}
 
+	location /updater { index index.php; }
+	location /ocm-provider { index index.php; }
+	location /ocs-provider { index index.php; }
+
 	location = /.well-known/carddav {
 		return 301 $scheme://$host:$server_port/remote.php/dav;
 	}

From 8a751571b78096dac8c3cb69bd3f5be8de49ac7c Mon Sep 17 00:00:00 2001
From: Adrian <adrian.dev@kousz.ch>
Date: Thu, 16 Apr 2020 23:45:18 +0200
Subject: [PATCH 37/37] Add dyndns and letsencrypt helpers

---
 dyndns/README.md                | 16 +++++++++++++++
 dyndns/dyndns-nsupdate          | 10 +++++++++
 dyndns/dyndns-update            | 19 +++++++++++++++++
 dyndns/example.com.nsupdate.txt | 12 +++++++++++
 dyndns/tsig.example.com.conf    |  4 ++++
 dyndns/update-example.com.sh    |  7 +++++++
 letsencrypt/README.md           | 36 +++++++++++++++++++++++++++++++++
 letsencrypt/config              |  5 +++++
 letsencrypt/dehydrated-manual   | 11 ++++++++++
 letsencrypt/dehydrated-nsupdate | 24 ++++++++++++++++++++++
 letsencrypt/example-hook        |  7 +++++++
 11 files changed, 151 insertions(+)
 create mode 100644 dyndns/README.md
 create mode 100755 dyndns/dyndns-nsupdate
 create mode 100755 dyndns/dyndns-update
 create mode 100644 dyndns/example.com.nsupdate.txt
 create mode 100644 dyndns/tsig.example.com.conf
 create mode 100755 dyndns/update-example.com.sh
 create mode 100644 letsencrypt/README.md
 create mode 100644 letsencrypt/config
 create mode 100755 letsencrypt/dehydrated-manual
 create mode 100755 letsencrypt/dehydrated-nsupdate
 create mode 100644 letsencrypt/example-hook

diff --git a/dyndns/README.md b/dyndns/README.md
new file mode 100644
index 0000000..21e1264
--- /dev/null
+++ b/dyndns/README.md
@@ -0,0 +1,16 @@
+# Dynamic DNS
+
+Edit example files to match your needs.
+
+```sh
+sudo mkdir /data/dns
+cp *example* dyndns* /data/dns
+
+chmod 600 /data/dns/tsig*
+```
+
+## Cronjob
+
+```sh
+/data/dns/update-example.com.sh
+```
diff --git a/dyndns/dyndns-nsupdate b/dyndns/dyndns-nsupdate
new file mode 100755
index 0000000..1876f7c
--- /dev/null
+++ b/dyndns/dyndns-nsupdate
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+DYN_DIR=/data/dns
+
+if test "x$DYN_TSIGKEY" = x; then DYN_TSIGKEY="$DYN_DIR/tsig.$DYN_DOMAIN.conf"; fi
+if test "x$DYN_NSUPDATE" = x; then DYN_NSUPDATE="$DYN_DIR/$DYN_DOMAIN.nsupdate.txt"; fi
+
+if test "x$1" != x; then
+	cat "$DYN_NSUPDATE" | sed s/%IP%/$1/g | nsupdate -v -k "$DYN_TSIGKEY"
+fi
diff --git a/dyndns/dyndns-update b/dyndns/dyndns-update
new file mode 100755
index 0000000..2eb07c8
--- /dev/null
+++ b/dyndns/dyndns-update
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+if test "x$DYN_SERVER" = x; then echo export DYN_SERVER=ns.example.com; exit=1; fi
+if test "x$DYN_DOMAIN" = x; then echo export DYN_DOMAIN=example.com; exit=1; fi
+if test "x$DYN_SCRIPT" = x; then echo export DYN_SCRIPT=/path/to/script; exit=1; fi
+if test "x$exit" = x1; then exit 1; fi
+
+if test "x$DYN_IPAPI" = x; then DYN_IPAPI=ifconfig.co; fi
+
+IPACTUAL=$(wget -qO - "$DYN_IPAPI")
+IPSERVER=$(dig +short $DYN_DOMAIN @$DYN_SERVER)
+
+if test "x$IPSERVER" = x -o "x$IPACTUAL" = x; then
+	: # ERROR: IP unknown
+elif test "x$IPSERVER" = "x$IPACTUAL"; then
+	: # INFO: IP not changed
+else
+	"$DYN_SCRIPT" $IPACTUAL
+fi
diff --git a/dyndns/example.com.nsupdate.txt b/dyndns/example.com.nsupdate.txt
new file mode 100644
index 0000000..8153944
--- /dev/null
+++ b/dyndns/example.com.nsupdate.txt
@@ -0,0 +1,12 @@
+server ns01.example.com
+zone example.com
+
+update del   example.com. TXT
+update del   example.com. A
+update del *.example.com. A
+
+update add   example.com. 86400 TXT "v=spf1 ip4:%IP%/32 -all"
+update add   example.com. 86400 A %IP%
+update add *.example.com. 86400 A %IP%
+
+send
diff --git a/dyndns/tsig.example.com.conf b/dyndns/tsig.example.com.conf
new file mode 100644
index 0000000..1cea1d4
--- /dev/null
+++ b/dyndns/tsig.example.com.conf
@@ -0,0 +1,4 @@
+key "tsig.example.com." {
+	algorithm hmac-sha256;
+	secret "YWRyaXVtLmFkcml1bS4uCg==";
+};
diff --git a/dyndns/update-example.com.sh b/dyndns/update-example.com.sh
new file mode 100755
index 0000000..e30dd4e
--- /dev/null
+++ b/dyndns/update-example.com.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+export DYN_DOMAIN=example.com
+export DYN_SERVER=ns01.example.com
+export DYN_SCRIPT=/data/dns/dyndns-nsupdate
+
+/data/dns/dyndns-update
diff --git a/letsencrypt/README.md b/letsencrypt/README.md
new file mode 100644
index 0000000..c1f3f51
--- /dev/null
+++ b/letsencrypt/README.md
@@ -0,0 +1,36 @@
+# Let's Encrypt
+
+Download Let's Encrypt client (only `dehydrated` needed):
+https://github.com/dehydrated-io/dehydrated/releases/latest
+
+```sh
+sudo mkdir -p /data/ssl/{configs,challenge}
+sudo chown -R admin: /data/ssl
+
+cp config dehydrated-* /data/ssl
+
+# List all domains for automatic renewal
+editor /data/ssl/domains.txt
+
+/data/ssl/dehydrated -r
+```
+
+To enable certificate renewal,
+`include snippets/letsencrypt` or put `redirect-ssl-all` in sites-enabled.
+
+## Cronjob
+
+```sh
+/data/ssl/dehydrated -c
+```
+
+## Wildcard Certificates
+
+```sh
+echo "service.example.com *.service.example.com" >> /data/ssl/domains.txt
+echo "CHALLENGETYPE=dns-01" >> /data/ssl/configs/service.example.com
+echo "HOOK=/data/ssl/dehydrated-hook" >> /data/ssl/configs/service.example.com
+```
+
+There are manual and nsupdate hooks.
+See [example-hook](example-hook) for an example nsupdate hook.
diff --git a/letsencrypt/config b/letsencrypt/config
new file mode 100644
index 0000000..8ddd42e
--- /dev/null
+++ b/letsencrypt/config
@@ -0,0 +1,5 @@
+DOMAINS_D=/data/ssl/configs
+WELLKNOWN=/data/ssl/challenge
+PRIVATE_KEY_RENEW=no
+KEYSIZE=2048
+# CONTACT_EMAIL=hostmaster@example.com
diff --git a/letsencrypt/dehydrated-manual b/letsencrypt/dehydrated-manual
new file mode 100755
index 0000000..0436362
--- /dev/null
+++ b/letsencrypt/dehydrated-manual
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+if test "x$1" = xdeploy_challenge; then
+	echo "Add the following record and press enter to continue:"
+	echo "_acme-challenge.$2. TXT $4"
+	read dummy
+elif test "x$1" = xclean_challenge; then
+	echo "Remove the record and press enter to continue:"
+	echo "_acme-challenge.$2. TXT $4"
+	read dummy
+fi
diff --git a/letsencrypt/dehydrated-nsupdate b/letsencrypt/dehydrated-nsupdate
new file mode 100755
index 0000000..52fd241
--- /dev/null
+++ b/letsencrypt/dehydrated-nsupdate
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+SCRIPT_TTL=30
+
+if test "x$LE_SERVER" = x; then echo export LE_SERVER=ns.example.com; exit=1; fi
+if test "x$LE_ZONE" = x; then echo export LE_ZONE=example.com; exit=1; fi
+if test "x$LE_TSIGKEY" = x; then echo export LE_TSIGKEY=/path/to/key; exit=1; fi
+if test "x$exit" = x1; then exit 1; fi
+
+if test "x$1" = xdeploy_challenge; then
+	nsupdate -v -k "$LE_TSIGKEY" <<- NSUPDATE
+		server $LE_SERVER
+		zone $LE_ZONE
+		update add _acme-challenge.$2. $SCRIPT_TTL TXT $4
+		send
+	NSUPDATE
+elif test "x$1" = xclean_challenge; then
+	nsupdate -v -k "$LE_TSIGKEY" <<- NSUPDATE
+		server $LE_SERVER
+		zone $LE_ZONE
+		update del _acme-challenge.$2. TXT
+		send
+	NSUPDATE
+fi
diff --git a/letsencrypt/example-hook b/letsencrypt/example-hook
new file mode 100644
index 0000000..80a49ad
--- /dev/null
+++ b/letsencrypt/example-hook
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+export LE_TSIGKEY=/data/dns/tsig.example.com.conf
+export LE_SERVER=ns01.example.com
+export LE_ZONE=example.com
+
+/data/ssl/dehydrated-nsupdate "$@"