summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorclarkzjw <[email protected]>2023-02-08 00:40:09 -0800
committerclarkzjw <[email protected]>2023-02-08 00:40:09 -0800
commit1204730924436ef9e1c7c49c9557837f9a5ed0e8 (patch)
tree129d79dfd11245751cee6d4082ff5d2f6e941610
parent9635ac4dedf69de5bff65785bcc16bef80b52d75 (diff)
downloadmail-master.tar.gz
fork https://github.com/mattsta/mailwebHEADmaster
-rw-r--r--ansible/README.md67
-rw-r--r--ansible/inventory/host_vars/mailmash/backup.yml18
-rw-r--r--ansible/inventory/host_vars/mailmash/certs.yml13
-rw-r--r--ansible/inventory/host_vars/mailmash/network.yml37
-rw-r--r--ansible/inventory/host_vars/mailmash/sieve.yml9
-rw-r--r--ansible/inventory/host_vars/webby/certs.yml23
-rw-r--r--ansible/inventory/host_vars/webby/network.yml12
-rw-r--r--ansible/inventory/host_vars/webby/nginx.yml77
-rw-r--r--ansible/inventory/inventory5
-rw-r--r--ansible/mailmash.yml15
-rw-r--r--ansible/roles/backup/meta/main.yml4
-rw-r--r--ansible/roles/backup/tasks/main.yml40
-rw-r--r--ansible/roles/backup/templates/borgmatic.yml.j236
-rw-r--r--ansible/roles/certreload/tasks/main.yml21
-rwxr-xr-xansible/roles/certs/files/leforward.py68
-rw-r--r--ansible/roles/certs/files/lets-encrypt-x3-cross-signed.pem27
-rw-r--r--ansible/roles/certs/tasks/main.yml153
-rw-r--r--ansible/roles/common/defaults/main.yml3
-rw-r--r--ansible/roles/common/files/ffdhe2048.pem8
-rw-r--r--ansible/roles/common/files/ffdhe3072.pem11
-rw-r--r--ansible/roles/common/files/ffdhe4096.pem13
-rw-r--r--ansible/roles/common/files/inputrc61
-rwxr-xr-xansible/roles/common/files/net-listeners.py334
-rwxr-xr-xansible/roles/common/files/ssh-transfer-only.sh11
-rw-r--r--ansible/roles/common/files/vimrc.local25
-rw-r--r--ansible/roles/common/handlers/main.yml20
-rw-r--r--ansible/roles/common/tasks/main.yml301
-rw-r--r--ansible/roles/disableFirewall/files/modprobe.d/blacklist-iptables.conf13
-rw-r--r--ansible/roles/disableFirewall/tasks/main.yml22
-rw-r--r--ansible/roles/dovecot/files/dovecot/authdb.sqlite3.emptybin0 -> 2048 bytes
-rw-r--r--ansible/roles/dovecot/files/dovecot/conf.d/10-acl.conf15
-rw-r--r--ansible/roles/dovecot/files/dovecot/conf.d/10-auth.conf46
-rw-r--r--ansible/roles/dovecot/files/dovecot/conf.d/10-mail.conf28
-rw-r--r--ansible/roles/dovecot/files/dovecot/conf.d/10-master.conf82
-rw-r--r--ansible/roles/dovecot/files/dovecot/conf.d/15-lda.conf7
-rw-r--r--ansible/roles/dovecot/files/dovecot/conf.d/15-mailboxes.conf24
-rw-r--r--ansible/roles/dovecot/files/dovecot/conf.d/20-imap.conf10
-rw-r--r--ansible/roles/dovecot/files/dovecot/conf.d/20-lmtp.conf5
-rw-r--r--ansible/roles/dovecot/files/dovecot/conf.d/90-imapsieve.conf18
-rw-r--r--ansible/roles/dovecot/files/dovecot/conf.d/90-sieve.conf34
-rw-r--r--ansible/roles/dovecot/files/dovecot/dovecot-sql.conf.ext150
-rw-r--r--ansible/roles/dovecot/files/dovecot/dovecot.conf19
-rw-r--r--ansible/roles/dovecot/files/dovecot/sieve-before.d/10-rspamd.sieve5
-rw-r--r--ansible/roles/dovecot/files/dovecot/sieve/report-ham.sieve23
-rw-r--r--ansible/roles/dovecot/files/dovecot/sieve/report-spam.sieve7
-rwxr-xr-xansible/roles/dovecot/files/dovecot/sieve/train-ham.sh1
-rwxr-xr-xansible/roles/dovecot/files/dovecot/sieve/train-spam.sh1
-rw-r--r--ansible/roles/dovecot/handlers/main.yml31
-rw-r--r--ansible/roles/dovecot/tasks/main.yml110
-rw-r--r--ansible/roles/dovecot/templates/dovecot/conf.d/10-ssl.conf.j222
-rw-r--r--ansible/roles/fail2ban/files/fail2ban/fail2ban.local0
-rw-r--r--ansible/roles/fail2ban/files/fail2ban/filter.d/postfix-rspamd.conf11
-rw-r--r--ansible/roles/fail2ban/files/fail2ban/jail.local59
-rw-r--r--ansible/roles/fail2ban/handlers/main.yml5
-rw-r--r--ansible/roles/fail2ban/tasks/main.yml28
-rw-r--r--ansible/roles/gpg/tasks/main.yml6
-rw-r--r--ansible/roles/network/tasks/main.yml40
-rw-r--r--ansible/roles/network/templates/network/custom.link6
-rw-r--r--ansible/roles/network/templates/network/custom.network38
-rw-r--r--ansible/roles/nginx/defaults/main.yml6
-rw-r--r--ansible/roles/nginx/files/conf.d/http.custom.conf84
-rw-r--r--ansible/roles/nginx/files/tls/ssl_ciphers_intermediate10
-rw-r--r--ansible/roles/nginx/files/tls/ssl_ciphers_modern7
-rw-r--r--ansible/roles/nginx/files/tls/ssl_ciphers_tls137
-rw-r--r--ansible/roles/nginx/files/tls/ssl_params55
-rw-r--r--ansible/roles/nginx/handlers/main.yml3
-rw-r--r--ansible/roles/nginx/tasks/main.yml118
-rw-r--r--ansible/roles/nginx/templates/basic-site.conf.j268
-rw-r--r--ansible/roles/pip3/tasks/main.yml6
-rw-r--r--ansible/roles/postfix/files/postfix/master.cf113
-rwxr-xr-xansible/roles/postfix/files/postfix/remap.sh9
-rw-r--r--ansible/roles/postfix/handlers/main.yml13
-rw-r--r--ansible/roles/postfix/tasks/main.yml50
-rw-r--r--ansible/roles/postfix/templates/postfix/main.cf.j2194
-rw-r--r--ansible/roles/ramdisk/tasks/main.yml13
-rw-r--r--ansible/roles/rspamd/files/redis/redis.conf21
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/classifier-bayes.conf4
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/greylist.conf1
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/milter_headers.conf3
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/mx_check.conf2
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/neural.conf33
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/neural_group.conf29
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/phishing.conf3
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/redis.conf2
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/replies.conf2
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/surbl.conf2
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/url_reputation.conf2
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/url_tags.conf2
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/worker-controller.inc39
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/worker-normal.inc3
-rw-r--r--ansible/roles/rspamd/files/rspamd/local.d/worker-proxy.inc16
-rw-r--r--ansible/roles/rspamd/handlers/main.yml11
-rw-r--r--ansible/roles/rspamd/meta/main.yml3
-rw-r--r--ansible/roles/rspamd/tasks/main.yml54
-rw-r--r--ansible/roles/sieve/tasks/main.yml20
-rwxr-xr-xansible/runner.sh41
96 files changed, 3325 insertions, 2 deletions
diff --git a/ansible/README.md b/ansible/README.md
index f26ed5a..7b2591b 100644
--- a/ansible/README.md
+++ b/ansible/README.md
@@ -1,3 +1,66 @@
1# mail 1mailweb: Matt's Mail and Web Ansible Config
2===========================================
2 3
3Self hosted mail server 4## What Is It?
5
6This is an Ansible playbook containing Ansible roles to configure
7my personal mail infrastructure components including:
8
9- postfix
10- dovecot
11- rspamd
12- borg-backup
13- fail2ban
14
15Also includes web components for installing multi-site `nginx` servers.
16
17`mailweb` was created to apply on modern Ubuntu servers with a current
18release version of Bionic 18.04 LTS (meaning: packages are deployed using
19the `apt` module only currently).
20
21A full writeup about this architecutre is at [Building a Production Mail Server in 2018](https://matt.sh/email2018)
22
23## Organization
24
25To avoid mistakes like accidentally publishing all your private keys or
26backup passphrases, we take advantage of Ansible's directory search hierarchy
27to isolate non-public content from role directories.
28
29For example: to avoid committing our private keys to the public repository,
30instead of putting keys in a subdirectory of the role itself (e.g. `./roles/certs/files/tls/site-key.pem`),
31we place them at the top level `file` path Ansible also searches (e.g. `./files/certs/tls/site-key.pem`).
32
33The same goes for `hosts_vars` and `group_vars` using this insight from the
34[Ansible docs](https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html#splitting-out-host-and-group-specific-data):
35
36> Tip: The `group_vars/` and `host_vars/` directories can exist in the playbook directory OR the inventory directory. If both paths exist, variables in the playbook directory will override variables set in the inventory directory.
37
38So, we place our sample vars in `inventory/{group,host}_vars` for publishing in this
39repository, then for actual usage we write production vars at the top level (which overrides the `inventory/*` vars).
40
41Now all we have to do is _not_ commit top level `files`, `group_vars`, and `host_vars` directories
42into the public repository (only commit on local internal branches). This is helped
43by our `.gitignore` in the public branch. View comments in `.gitignore` for more details
44about private usage.
45
46
47## Contributing
48
49Contributions welcome! Any PRs about improving configs towards security, usability, performance, and cross platform growth is encouraged.
50
51If you want to make changes for your own needs (but maybe not for _everybody's_ needs), feel free to submit the changes and just guard them with enable `when` blocks activated by config variables.
52
53Hopefully we can keep this architecture alive as its package components and underlying distributions grow over time.
54
55### Potential Problems
56
57- Not extensively tested outside my personal environment
58 - there's probably default vars missing in places; feel free to submit fixes
59
60### Acceptable Improvements
61
62- Feel free to submit better cross-platform integration
63 - cross-OS package management (`if centos` vs. `if debian` etc)
64 - should include better version checking/version pinning so we don't try to load 2018 configs into older servers not supporting modern options
65 - cross-OS config file locations, handlers, etc
66- Update config files when [newer standards or features](https://matt.sh/web2018) get implemented and released
diff --git a/ansible/inventory/host_vars/mailmash/backup.yml b/ansible/inventory/host_vars/mailmash/backup.yml
new file mode 100644
index 0000000..aa5fcc4
--- /dev/null
+++ b/ansible/inventory/host_vars/mailmash/backup.yml
@@ -0,0 +1,18 @@
1---
2backup:
3 # Define a host in your ~/.ssh/config with username, host, and private key.
4 # Maintaining your ~/.ssh/config is out of scope for our implementation here.
5 host: rsn-backup
6
7 # directories to backup can be amended at any time
8 dirs:
9 - /var/mail
10 - /var/lib/fail2ban
11 - /var/lib/redis
12
13 # phrase is from: borg init --remote-path=borg1 --encryption=repokey-blake2 rsn-backup:mailmash
14 phrase: your phrase goes here
15
16 # your runAs user should be able to ssh to the backup host without a password
17 # (remote login user is configured by Host->User in this user's ~/.ssh/config)
18 runAs: root
diff --git a/ansible/inventory/host_vars/mailmash/certs.yml b/ansible/inventory/host_vars/mailmash/certs.yml
new file mode 100644
index 0000000..fd6dde0
--- /dev/null
+++ b/ansible/inventory/host_vars/mailmash/certs.yml
@@ -0,0 +1,13 @@
1---
2certs:
3 # Copy only these private keys and certs from ansible into the system
4 requested:
5 - yourmail.server.com
6
7 # we receive our certs by scp from the main cert hosting service elsewhere
8 receiver: true
9
10 # These users have ansible-controlled ssh private keys
11 # (mainly for automated backups right now)
12 sshKeysForUsers:
13 - root
diff --git a/ansible/inventory/host_vars/mailmash/network.yml b/ansible/inventory/host_vars/mailmash/network.yml
new file mode 100644
index 0000000..e290f7b
--- /dev/null
+++ b/ansible/inventory/host_vars/mailmash/network.yml
@@ -0,0 +1,37 @@
1---
2network:
3 # These interface names are used to pull IP addresses into templates.
4 # interface.public has the IP we use to open ports to the world (mail, web, etc)
5 # interface.private has the IP for private services (ssh login, reporting, etc)
6 # Right now we don't support multiple IPs per interface, we just grab the IPv4
7 # address as presented by ansible fact e.g. 'ansible_{{interface.public}}.ipv4.address'
8 interface:
9 private: ens3
10 public: ens4
11 hostname:
12 # network.hostname.public is used populate templates with
13 # server's public hostname, including:
14 # - TLS certs to use with this pattern:
15 # /etc/ssl/[hostname]-cert-combined.pem
16 # /etc/ssl/[hostname]-key.pem
17 # - Postfix config parameter "myhostname"
18 public: yourmail.server.com
19
20 # Below is almost (almost!) the netplan schema with three changes:
21 # - only one "interface" per system because ansible can't seem to generate multiple
22 # templates from one yaml dict?
23 # - so, the 'interface' key is added, other wise it would just be ethernets: ens4: ...
24 # - also, 'addresses' needs per-address broadcast, so addresses are now lists
25 # of dicts, so 'ip' is a new name as is 'broadcast'
26 ethernets:
27 interface: ens4
28 macaddress: 02:00:00:7d:ca:ab
29 networks:
30 - subnet: 4.4.4.0/30
31 gateway: 5.5.5.5
32 addresses:
33 - 4.4.4.4/32
34 routes:
35 - to: 0.0.0.0/0
36 via: 5.5.5.5
37 on-link: true
diff --git a/ansible/inventory/host_vars/mailmash/sieve.yml b/ansible/inventory/host_vars/mailmash/sieve.yml
new file mode 100644
index 0000000..635fb4e
--- /dev/null
+++ b/ansible/inventory/host_vars/mailmash/sieve.yml
@@ -0,0 +1,9 @@
1---
2sieve:
3 users:
4 # username field is the filename [username].sieve in files/
5 # The sieve file gets stored in vmail user directory [domain]/[user]/sieve/
6 # We could improve this because it's clearly redundant...
7 - username: [email protected]
8 domain: server.com
9 user: username
diff --git a/ansible/inventory/host_vars/webby/certs.yml b/ansible/inventory/host_vars/webby/certs.yml
new file mode 100644
index 0000000..c38b80d
--- /dev/null
+++ b/ansible/inventory/host_vars/webby/certs.yml
@@ -0,0 +1,23 @@
1---
2certs:
3 # Copy only these private keys and certs from ansible into the system
4 keyTypes:
5 - rsa2048
6 - prime256v1
7
8# requested can EITHER be:
9# - just a list of hostnames (then we depoly all 'keyTypes' for each hostname)
10# - or, a mapping of, e.g.:
11# - host: example1.com
12# type: rsa2048
13# - host: example1.com
14# type: prime256v1
15 required:
16 - example1.com
17 - example2.com
18 - example3.com
19
20 # These users have ansible-controlled ssh private keys
21 # (mainly for automated backups right now)
22 sshKeysForUsers: []
23 # - root
diff --git a/ansible/inventory/host_vars/webby/network.yml b/ansible/inventory/host_vars/webby/network.yml
new file mode 100644
index 0000000..39eb141
--- /dev/null
+++ b/ansible/inventory/host_vars/webby/network.yml
@@ -0,0 +1,12 @@
1---
2network:
3 # These interface names are used to pull IP addresses into templates.
4 # interface.public has the IP we use to open ports to the world (mail, web, etc)
5 # interface.private has the IP for private services (ssh login, reporting, etc)
6 # Right now we don't support multiple IPs per interface, we just grab the IPv4
7 # address as presented by ansible fact e.g. 'ansible_{{interface.public}}.ipv4.address'
8 interface:
9 private: ens3
10 public: ens4
11 hostname:
12 public: webby
diff --git a/ansible/inventory/host_vars/webby/nginx.yml b/ansible/inventory/host_vars/webby/nginx.yml
new file mode 100644
index 0000000..87976dc
--- /dev/null
+++ b/ansible/inventory/host_vars/webby/nginx.yml
@@ -0,0 +1,77 @@
1---
2nginx:
3 # Google webmaster tools wants this same filename on all hosts
4 google:
5 siteKey: googleYOURKEYHERE
6 siteKeyServeDir: /srv/web/files/
7
8 # ssl can be "modern" or "tls13" or anything else means default
9 ssl: default
10
11 # These configs are directly uploaded as saved config files from files/nginx/<sitename>
12 complex:
13 - example4.com
14 - example5.com
15
16 basic: []
17 # These configs are generated by template description below.
18 # 'customConfig' is nginx directives placed for your http2 server block.
19 # basic:
20 # - domain: "example.com"
21 # uri:
22 # - path: /
23 #
24 # - domain: example2.com
25 # uri:
26 # - path: /
27 # appServer: "http://127.0.0.1:7780"
28 # customConfig: |
29 # keepalive_timeout 5 5;
30 # keepalive_requests 200;
31 #
32 # proxy_intercept_errors on;
33 # error_page 502 =503 @noserver;
34 # error_page 503 =503 @noserver;
35 # error_page 504 =503 @noserver;
36 #
37 # location @noserver {
38 # root /home/matt/repos/matt-prod/priv;
39 # charset utf-8;
40 # rewrite ^(.*)$ /noserver.txt break;
41 # }
42 #
43 # location /src/ {
44 # proxy_pass http://127.0.0.1:7780/$request_uri;
45 # add_header Cache-Control public;
46 # expires +5m;
47 # if ($args) {
48 # expires +1y;
49 # }
50 # }
51 #
52 # location /style {
53 # proxy_pass http://127.0.0.1:7780/$request_uri;
54 # add_header Cache-Control public;
55 # expires +5m;
56 # if ($args) {
57 # expires +1y;
58 # }
59 # }
60 #
61 # location /js {
62 # proxy_pass http://127.0.0.1:7780/$request_uri;
63 # add_header Cache-Control public;
64 # expires +5m;
65 # if ($args) {
66 # expires +1y;
67 # }
68 # }
69 #
70 # location /favicon.ico {
71 # empty_gif;
72 # }
73 #
74 # location /files {
75 # expires max;
76 # root /srv/web/matt.sh;
77 # }
diff --git a/ansible/inventory/inventory b/ansible/inventory/inventory
new file mode 100644
index 0000000..3999869
--- /dev/null
+++ b/ansible/inventory/inventory
@@ -0,0 +1,5 @@
1[mail]
2mailmash ansible_python_interpreter=/usr/bin/python3
3
4[web]
5webby ansible_python_interpreter=/usr/bin/python3
diff --git a/ansible/mailmash.yml b/ansible/mailmash.yml
new file mode 100644
index 0000000..15603b4
--- /dev/null
+++ b/ansible/mailmash.yml
@@ -0,0 +1,15 @@
1---
2- hosts: mail
3 remote_user: clarkzjw
4 become: yes
5 roles:
6 - common
7 - certs
8 - network
9 - disableFirewall
10 - rspamd
11 - dovecot
12 - sieve
13 - postfix
14 - fail2ban
15 - backup
diff --git a/ansible/roles/backup/meta/main.yml b/ansible/roles/backup/meta/main.yml
new file mode 100644
index 0000000..023023d
--- /dev/null
+++ b/ansible/roles/backup/meta/main.yml
@@ -0,0 +1,4 @@
1---
2dependencies:
3 # borgmatic is inside a pip3 package
4 - role: pip3
diff --git a/ansible/roles/backup/tasks/main.yml b/ansible/roles/backup/tasks/main.yml
new file mode 100644
index 0000000..fedc68b
--- /dev/null
+++ b/ansible/roles/backup/tasks/main.yml
@@ -0,0 +1,40 @@
1---
2- name: install borgbackup
3 apt:
4 pkg: borgbackup
5 state: latest
6
7- name: install borgmatic
8 pip:
9 name: borgmatic
10 state: latest
11
12- name: create backup config dir
13 file:
14 path: /etc/borgmatic.d
15 owner: "{{ backup.runAs }}"
16 mode: 0700
17 state: directory
18
19# Create backup config for entire server
20# Ideally we only have one type of data to backup per server and the rest
21# can be re-constructed as necessary through auto-deploy processes
22- name: populate borgmatic config with details for hosts
23 template:
24 src: borgmatic.yml.j2
25 dest: /etc/borgmatic.d/system.backup.yml
26 owner: "{{ backup.runAs }}"
27 mode: 0600
28
29# Note: right now we aren't populating an 'excludes' file
30# If we need 'excludes' in the future, append '--excludes [excludesDirsFile]'
31# ALSO NOTE: your backup.runAs user MUST MANUALLY ACCEPT THE BACKUP HOST SSH KEY
32# Backup will stall if unattended ssh sees new host fingerprint needing approval
33- name: install backup crontab
34 cron:
35 name: "Backup Offsite"
36 minute: 32
37 hour: 3
38 job: "borgmatic --verbosity 1 -c /etc/borgmatic.d/system.backup.yml"
39 user: "{{ backup.runAs }}"
40 cron_file: backup_offsite
diff --git a/ansible/roles/backup/templates/borgmatic.yml.j2 b/ansible/roles/backup/templates/borgmatic.yml.j2
new file mode 100644
index 0000000..864c8a6
--- /dev/null
+++ b/ansible/roles/backup/templates/borgmatic.yml.j2
@@ -0,0 +1,36 @@
1location:
2 # List of source directories to backup. Globs are expanded.
3 source_directories:
4{% for dir in backup.dirs %}
5 - {{ dir }}
6{% endfor %}
7
8 # Paths to local or remote repositories.
9 repositories:
10 - {{ backup.host }}:{{ inventory_hostname }}
11
12 one_file_system: True
13 remote_path: borg1
14
15 # Any paths matching these patterns are excluded from backups.
16 exclude_patterns:
17 - /home/*/.cache
18
19storage:
20 encryption_passphrase: {{ backup.phrase }}
21 compression: lz4
22
23retention:
24 # Retention policy for how many backups to keep in each category.
25 keep_within: 3H
26 keep_daily: 7
27 keep_weekly: 2
28 keep_monthly: 3
29
30consistency:
31 # List of consistency checks to run: "repository", "archives", or both.
32 checks:
33 - repository
34 - archives
35
36 check_last: 1
diff --git a/ansible/roles/certreload/tasks/main.yml b/ansible/roles/certreload/tasks/main.yml
new file mode 100644
index 0000000..0e4fcc7
--- /dev/null
+++ b/ansible/roles/certreload/tasks/main.yml
@@ -0,0 +1,21 @@
1---
2# This is a hack because ansible can't trigger handlers if they don't
3# exist, so we can't have our 'certs' role unconditionally fire things
4# like "reload nginx" and "reload postfix" because those don't exist
5# in every deployment.
6# As a hack, just check if /etc/ssl was recently modified then reload
7- name: check certificate update recency
8 stat:
9 path: /etc/ssl
10 register: statSSL
11
12# Have to mock a command resulting in some "changed" status so ansible
13# allows itself to trigger handlers.
14# The actual restriction on this handler is the 'when' clause, not
15# the command itself.
16- name: reload because certs are newish
17 command: /bin/true
18 when: ((ansible_date_time.epoch |int) - (statSSL.stat.mtime |int)) < 300
19 notify:
20 - "{{ item }}"
21 loop: "{{ certreload.notifiers }}"
diff --git a/ansible/roles/certs/files/leforward.py b/ansible/roles/certs/files/leforward.py
new file mode 100755
index 0000000..dccbac1
--- /dev/null
+++ b/ansible/roles/certs/files/leforward.py
@@ -0,0 +1,68 @@
1#!/usr/bin/env python3
2
3""" Run a single-purpose HTTP server.
4
5Server takes all GET requests and redirects them to a new host
6if the request URI starts with SUBPATH, otherwise returns 404.
7
8Requests are redirected to the URL provided by --baseurl. """
9
10import socketserver
11import http.server
12import argparse
13import sys
14
15
16CHALLENGE_HOST = None
17SUBPATH = "/.well-known/acme-challenge"
18
19
20class RedirectChallenges(http.server.BaseHTTPRequestHandler):
21 def do_GET(self):
22 if self.path.startswith(SUBPATH):
23 self.send_response(301)
24 self.send_header('Location', f"{CHALLENGE_HOST}{self.path}")
25 else:
26 self.send_response(404)
27
28 self.end_headers()
29
30
31class ReusableServer(socketserver.TCPServer):
32 """ Allow TCPServer to reuse host address.
33
34 Without setting 'allow_reuse_address', we can get stuck in
35 TIME_WAIT after being killed and the stale state stops a new
36 server from attaching to the port."""
37
38 allow_reuse_address = True
39
40
41if __name__ == "__main__":
42 parser = argparse.ArgumentParser(
43 description="Redirect all URIs with matching prefix to another host")
44 parser.add_argument(
45 '--baseurl',
46 dest='baseurl',
47 required=True,
48 help="Destination URL for all matching URIs on this server")
49
50 args = parser.parse_args()
51 CHALLENGE_HOST = args.baseurl
52
53 if not CHALLENGE_HOST.startswith("http"):
54 print("Redirect URL must be a full URL starting with http")
55 sys.exit(1)
56
57 # If user gave us a trailing slash URL, remove slash.
58 if CHALLENGE_HOST[-1] == "/":
59 CHALLENGE_HOST = CHALLENGE_HOST[:-1]
60
61 serverAddress = ('', 80)
62
63 # Note: if running remotely by an SSH command, you MUST launch with '-t':
64 # > ssh -t me@otherhost leforward.py --baseurl http://otherserver.com
65 # If you omit '-t' the listening server won't terminate when you kill the
66 # ssh session, which probably isn't what you want.
67 with ReusableServer(serverAddress, RedirectChallenges) as httpd:
68 httpd.serve_forever()
diff --git a/ansible/roles/certs/files/lets-encrypt-x3-cross-signed.pem b/ansible/roles/certs/files/lets-encrypt-x3-cross-signed.pem
new file mode 100644
index 0000000..0002462
--- /dev/null
+++ b/ansible/roles/certs/files/lets-encrypt-x3-cross-signed.pem
@@ -0,0 +1,27 @@
1-----BEGIN CERTIFICATE-----
2MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
3MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
4DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
5SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
6GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
7AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
8q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
9SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
10Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
11a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
12/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
13AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
14CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
15bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
16c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
17VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
18ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
19MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
20Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
21AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
22uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
23wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
24X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
25PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
26KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
27-----END CERTIFICATE-----
diff --git a/ansible/roles/certs/tasks/main.yml b/ansible/roles/certs/tasks/main.yml
new file mode 100644
index 0000000..e83a640
--- /dev/null
+++ b/ansible/roles/certs/tasks/main.yml
@@ -0,0 +1,153 @@
1---
2- name: remove default ubuntu key
3 file:
4 path: /etc/ssl/private/ssl-cert-snakeoil.key
5 state: absent
6
7- name: create cert maint group
8 group:
9 name: certmaint
10 gid: 1070
11 state: present
12
13- name: create cert maint user
14 user:
15 name: certmaint
16 uid: 1070
17 group: ssl-cert
18 groups:
19 - certmaint
20 shell: /bin/sh
21 create_home: yes
22 state: present
23
24#- name: allow certmaint to maint certs and keys (default)
25# acl:
26# path: /etc/ssl/
27# etype: user
28# entity: certmaint
29# permissions: rw
30# default: yes
31# recursive: yes
32# state: present
33# no_log: true
34
35#- name: allow certmaint to maint certs and keys (actual certs)
36# acl:
37# path: /etc/ssl/
38# etype: user
39# entity: certmaint
40# permissions: rwx
41# state: present
42# no_log: true
43
44#- name: allow certmaint to maint certs and keys (actual keys)
45# acl:
46# path: /etc/ssl/private/
47# etype: user
48# entity: certmaint
49# permissions: rwx
50# state: present
51# no_log: true
52
53# Keys are private: only owner can read/write, and only group can read
54- name: populate required keys (common types)
55 copy:
56 src: "tls/private/{{ item[0] }}-key.{{ item[1] }}.pem"
57 dest: /etc/ssl/private/
58 mode: 0640
59 owner: certmaint
60 group: ssl-cert
61 loop: "{{ certs.required |product(certs.keyTypes) |list }}"
62 when: certs.required[0] is string
63
64
65# Certs are owned by 'certmaint' so user 'certmaint' can update them over scp
66# Certs are public (obviously)
67- name: populate required certs (common types)
68 copy:
69 src: "tls/{{ item[0] }}-cert-combined.{{ item[1] }}.pem"
70 dest: /etc/ssl/
71 mode: 0644
72 owner: certmaint
73 loop: "{{ certs.required |product(certs.keyTypes) |list }}"
74 when: certs.required[0] is string
75
76
77
78# Keys are private: only owner can read/write, and only group can read
79- name: populate required keys (specific types)
80 copy:
81 src: "tls/private/{{ item.host }}-key.{{ item.type }}.pem"
82 dest: /etc/ssl/private/
83 mode: 0640
84 owner: certmaint
85 group: ssl-cert
86 loop: "{{ certs.required }}"
87 when: certs.required[0] is mapping
88
89# Certs are owned by 'certmaint' so user 'certmaint' can update them over scp
90# Certs are public (obviously)
91- name: populate required certs (specific types)
92 copy:
93 src: "tls/{{ item.host }}-cert-combined.{{ item.type }}.pem"
94 dest: /etc/ssl/
95 mode: 0644
96 owner: certmaint
97 loop: "{{ certs.required }}"
98 when: certs.required[0] is mapping
99
100
101
102- name: plop LE cert chain
103 copy:
104 src: "tls/lets-encrypt-x3-cross-signed.pem"
105 dest: /etc/ssl/
106 mode: 0644
107 owner: certmaint
108
109- name: plop remote LE challenge redirector
110 copy:
111 src: leforward.py
112 dest: /usr/local/bin/
113 mode: 0755
114 when:
115 - certs.receiver is defined and certs.receiver
116
117
118# Retrieve all users on this host (creates variable 'passwd' containing results)
119- name: get all user details so we can populate home directories
120 getent:
121 database: passwd
122
123# Copy users/hostname/username contents into remote home directory
124- name: verify explicit user keys exist as expected
125 copy:
126 src: "users/{{ inventory_hostname }}/{{ item }}/"
127 # [item][4] is [username][homedir] where /etc/passwd is tokenized on ':'
128 # and username becomes the key with remaining fields indexed by integers
129 dest: "{{ getent_passwd[item][4] }}"
130 mode: 0600
131 owner: "{{ item }}"
132 directory_mode: 0700
133 loop: "{{ certs.sshKeysForUsers }}"
134
135# TODO: we could make one key per action then restrict actions by ssh key.
136# (postfix key, dovecot key, nginx key, leforward key)
137- name: verify certmaint receiver key exists
138 copy:
139 src: "users/certmaint/"
140 dest: "{{ getent_passwd[item][4] }}"
141 mode: 0600
142 owner: "{{ item }}"
143 directory_mode: 0700
144 loop:
145 - certmaint
146
147- name: allow certmaint group to sudo reload relevant services
148 lineinfile:
149 path: /etc/sudoers.d/certmaint_reloads
150 regexp: "^%certmaint"
151 line: "%certmaint ALL = (root) NOPASSWD: /usr/sbin/service postfix reload, /usr/sbin/service dovecot reload, /usr/sbin/service nginx reload"
152 create: yes
153 mode: 0440
diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml
new file mode 100644
index 0000000..2410e85
--- /dev/null
+++ b/ansible/roles/common/defaults/main.yml
@@ -0,0 +1,3 @@
1---
2grub:
3 extras: ""
diff --git a/ansible/roles/common/files/ffdhe2048.pem b/ansible/roles/common/files/ffdhe2048.pem
new file mode 100644
index 0000000..9b182b7
--- /dev/null
+++ b/ansible/roles/common/files/ffdhe2048.pem
@@ -0,0 +1,8 @@
1-----BEGIN DH PARAMETERS-----
2MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
3+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
487VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
5YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
67MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
7ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
8-----END DH PARAMETERS-----
diff --git a/ansible/roles/common/files/ffdhe3072.pem b/ansible/roles/common/files/ffdhe3072.pem
new file mode 100644
index 0000000..fb31ccd
--- /dev/null
+++ b/ansible/roles/common/files/ffdhe3072.pem
@@ -0,0 +1,11 @@
1-----BEGIN DH PARAMETERS-----
2MIIBiAKCAYEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
3+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
487VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
5YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
67MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
7ssbzSibBsu/6iGtCOGEfz9zeNVs7ZRkDW7w09N75nAI4YbRvydbmyQd62R0mkff3
87lmMsPrBhtkcrv4TCYUTknC0EwyTvEN5RPT9RFLi103TZPLiHnH1S/9croKrnJ32
9nuhtK8UiNjoNq8Uhl5sN6todv5pC1cRITgq80Gv6U93vPBsg7j/VnXwl5B0rZsYu
10N///////////AgEC
11-----END DH PARAMETERS-----
diff --git a/ansible/roles/common/files/ffdhe4096.pem b/ansible/roles/common/files/ffdhe4096.pem
new file mode 100644
index 0000000..3cf0fcb
--- /dev/null
+++ b/ansible/roles/common/files/ffdhe4096.pem
@@ -0,0 +1,13 @@
1-----BEGIN DH PARAMETERS-----
2MIICCAKCAgEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
3+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
487VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
5YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
67MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
7ssbzSibBsu/6iGtCOGEfz9zeNVs7ZRkDW7w09N75nAI4YbRvydbmyQd62R0mkff3
87lmMsPrBhtkcrv4TCYUTknC0EwyTvEN5RPT9RFLi103TZPLiHnH1S/9croKrnJ32
9nuhtK8UiNjoNq8Uhl5sN6todv5pC1cRITgq80Gv6U93vPBsg7j/VnXwl5B0rZp4e
108W5vUsMWTfT7eTDp5OWIV7asfV9C1p9tGHdjzx1VA0AEh/VbpX4xzHpxNciG77Qx
11iu1qHgEtnmgyqQdgCpGBMMRtx3j5ca0AOAkpmaMzy4t6Gh25PXFAADwqTs6p+Y0K
12zAqCkc3OyX3Pjsm1Wn+IpGtNtahR9EGC4caKAH5eZV9q//////////8CAQI=
13-----END DH PARAMETERS-----
diff --git a/ansible/roles/common/files/inputrc b/ansible/roles/common/files/inputrc
new file mode 100644
index 0000000..47e7275
--- /dev/null
+++ b/ansible/roles/common/files/inputrc
@@ -0,0 +1,61 @@
1# do not bell on tab-completion
2#set bell-style none
3
4set meta-flag on
5set input-meta on
6set convert-meta off
7set output-meta on
8
9# Completed names which are symbolic links to
10# directories have a slash appended.
11set mark-symlinked-directories on
12
13$if mode=emacs
14
15# for linux console and RH/Debian xterm
16"\e[1~": beginning-of-line
17"\e[4~": end-of-line
18# commented out keymappings for pgup/pgdown to reach begin/end of history
19#"\e[5~": beginning-of-history
20#"\e[6~": end-of-history
21"\e[5~": history-search-backward
22"\e[6~": history-search-forward
23"\e[3~": delete-char
24"\e[2~": quoted-insert
25"\e[5C": forward-word
26"\e[5D": backward-word
27"\e[1;5C": forward-word
28"\e[1;5D": backward-word
29
30# for rxvt
31"\e[8~": end-of-line
32"\eOc": forward-word
33"\eOd": backward-word
34
35# for non RH/Debian xterm, can't hurt for RH/DEbian xterm
36"\eOH": beginning-of-line
37"\eOF": end-of-line
38
39# for freebsd console
40"\e[H": beginning-of-line
41"\e[F": end-of-line
42$endif
43
44# cd d<TAB> will match documents or Documents
45set completion-ignore-case on
46
47# front-of-command up and down completion
48"\e[A":history-search-backward
49"\e[B":history-search-forward
50
51# This is the magic command.
52# Enables sane tcsh-like ctrl-d completion showing.
53Control-d:delete-char-or-list
54
55# oddly, menu-complete makes the menu *not* appear
56# just cycle through each option
57Tab:menu-complete
58
59# stop asking if I "Really want to see 102 completions"
60set completion-query-items 350
61set page-completions off
diff --git a/ansible/roles/common/files/net-listeners.py b/ansible/roles/common/files/net-listeners.py
new file mode 100755
index 0000000..f8b39cd
--- /dev/null
+++ b/ansible/roles/common/files/net-listeners.py
@@ -0,0 +1,334 @@
1#!/usr/bin/env python3
2
3""" Output a colorized list of listening addresses with owners.
4
5This tool parses files in /proc directly to obtain the list
6of IPv4 and IPv6 addresses listening on tcp, tcp6, udp, and udp6 ports
7also with pids of processes responsible for the listening.
8
9Due to permission restrictions on Linux, script must be run as root
10to determine which pids match which listening sockets.
11
12This is also something like:
13 osqueryi "select po.pid, rtrim(p.cmdline), po.family, po.local_address, po.local_port from process_open_sockets as po JOIN processes as p ON po.pid=p.pid WHERE po.state='LISTEN';"
14
15"""
16
17import collections
18import subprocess
19import codecs
20import socket
21import struct
22import glob
23import sys
24import re
25import os
26
27TERMINAL_WIDTH = "/usr/bin/tput cols" # could also be "stty size"
28
29ONLY_LOWEST_PID = False
30
31# oooh, look, a big dirty global dict collecting all our data without being
32# passed around! call the programming police!
33inodes = {}
34
35
36class Color:
37 HEADER = '\033[95m'
38 OKBLUE = '\033[94m'
39 OKGREEN = '\033[92m'
40 WARNING = '\033[93m'
41 FAIL = '\033[91m'
42 BOLD = '\033[1m'
43 UNDERLINE = '\033[4m'
44 END = '\033[0m'
45
46
47COLOR_HEADER = Color.HEADER
48COLOR_OKAY = Color.OKBLUE
49COLOR_WARNING = Color.FAIL
50COLOR_END = Color.END
51
52# This should capture:
53# 127.0.0.0/8
54# 192.168.0.0/16
55# 10.0.0.0/8
56# 169.254.0.0/16
57# 172.16.0.0/12
58# ::1
59# fe80::/10
60# fc00::/7
61# fd00::/8
62NON_ROUTABLE_REGEX = r"""^((127\.) |
63 (192\.168\.) |
64 (10\.) |
65 (169\.254\.) |
66 (172\.1[6-9]\.) |
67 (172\.2[0-9]\.) |
68 (172\.3[0-1]\.) |
69 (::1) |
70 ([fF][eE]80)
71 ([fF][cCdD]))"""
72likelyLocalOnly = re.compile(NON_ROUTABLE_REGEX, re.VERBOSE)
73
74
75def run(thing):
76 """ Run any string as an async command invocation. """
77 # We don't use subprocess.check_output because we want to run all
78 # processes async
79 return subprocess.Popen(thing.split(), stdout=subprocess.PIPE)
80
81
82def readOutput(ranCommand):
83 """ Return array of rows split by newline from previous invocation. """
84 stdout, stderr = ranCommand.communicate()
85 return stdout.decode('utf-8').strip().splitlines()
86
87
88def procListeners():
89 """ Wrapper to parse all IPv4 tcp udp, and, IPv6 tcp6 udp6 listeners. """
90
91 def processProc(name):
92 """ Process IPv4 and IPv6 versions of listeners based on ``name``.
93
94 ``name`` is either 'udp' or 'tcp' so we parse, for each ``name``:
95 - /proc/net/[name]
96 - /proc/net/[name]6
97
98 As in:
99 - /proc/net/tcp
100 - /proc/net/tcp6
101 - /proc/net/udp
102 - /proc/net/udp6
103 """
104
105 def ipv6(addr):
106 """ Convert /proc IPv6 hex address into standard IPv6 notation. """
107 # turn ASCII hex address into binary
108 addr = codecs.decode(addr, "hex")
109
110 # unpack into 4 32-bit integers in big endian / network byte order
111 addr = struct.unpack('!LLLL', addr)
112
113 # re-pack as 4 32-bit integers in system native byte order
114 addr = struct.pack('@IIII', *addr)
115
116 # now we can use standard network APIs to format the address
117 addr = socket.inet_ntop(socket.AF_INET6, addr)
118 return addr
119
120 def ipv4(addr):
121 """ Convert /proc IPv4 hex address into standard IPv4 notation. """
122 # Instead of codecs.decode(), we can just convert a 4 byte hex
123 # string to an integer directly using python radix conversion.
124 # Basically, int(addr, 16) EQUALS:
125 # aOrig = addr
126 # addr = codecs.decode(addr, "hex")
127 # addr = struct.unpack(">L", addr)
128 # assert(addr == (int(aOrig, 16),))
129 addr = int(addr, 16)
130
131 # system native byte order, 4-byte integer
132 addr = struct.pack("=L", addr)
133 addr = socket.inet_ntop(socket.AF_INET, addr)
134 return addr
135
136 isUDP = name == "udp"
137
138 # Iterate four files: /proc/net/{tcp,udp}{,6}
139 # ipv4 has no prefix, while ipv6 has 6 appended.
140 for ver in ["", "6"]:
141 with open(f"/proc/net/{name}{ver}", 'r') as proto:
142 proto = proto.read().splitlines()
143 proto = proto[1:] # drop header row
144
145 for cxn in proto:
146 cxn = cxn.split()
147
148 # /proc/net/udp{,6} uses different constants for LISTENING
149 if isUDP:
150 # These constants are based on enum offsets inside
151 # the Linux kernel itself. They aren't likely to ever
152 # change since they are hardcoded in utilities.
153 isListening = cxn[3] == "07"
154 else:
155 isListening = cxn[3] == "0A"
156
157 # Right now this is a single-purpose tool so if process is
158 # not listening, we avoid further processing of this row.
159 if not isListening:
160 continue
161
162 ip, port = cxn[1].split(':')
163 if ver:
164 ip = ipv6(ip)
165 else:
166 ip = ipv4(ip)
167
168 port = int(port, 16)
169 inode = cxn[9]
170
171 # We just use a list here because creating a new sub-dict
172 # for each entry was noticably slower than just indexing
173 # into lists.
174 inodes[int(inode)] = [ip, port, f"{name}{ver}"]
175
176 processProc("tcp")
177 processProc("udp")
178
179
180def appendToInodePidMap(fd, inodePidMap):
181 """ Take a full path to /proc/[pid]/fd/[fd] for reading.
182
183 Populates both pid and full command line of pid owning an inode we
184 are interested in.
185
186 Basically finds if any inodes on this pid is a listener we previously
187 recorded into our ``inodes`` dict. """
188 _, _, pid, _, _ = fd.split('/')
189 try:
190 target = os.readlink(fd)
191 except FileNotFoundError:
192 # file vanished, can't do anything else
193 return
194
195 if target.startswith("socket"):
196 ostype, inode = target.split(':')
197 # strip brackets from fd string (it looks like: [fd])
198 inode = int(inode[1:-1])
199 inodePidMap[inode].append(int(pid))
200
201
202def addProcessNamesToInodes():
203 """ Loop over every fd in every process in /proc.
204
205 The only way to map an fd back to a process is by looking
206 at *every* processes fd and extracting backing inodes.
207
208 It's basically like a big awkward database join where you don't
209 have an index on the field you want.
210
211 Also, due to Linux permissions (and Linux security concerns),
212 only the root user can read fd listing of processes not owned
213 by the current user. """
214
215 # glob glob glob it all
216 allFDs = glob.iglob("/proc/*/fd/*")
217 inodePidMap = collections.defaultdict(list)
218
219 for fd in allFDs:
220 appendToInodePidMap(fd, inodePidMap)
221
222 for inode in inodes:
223 if inode in inodePidMap:
224 for pid in inodePidMap[inode]:
225 try:
226 with open(f"/proc/{pid}/cmdline", 'r') as cmd:
227 # /proc command line arguments are delimited by
228 # null bytes, so undo that here...
229 cmdline = cmd.read().split('\0')
230 inodes[inode].append((pid, cmdline))
231 except BaseException:
232 # files can vanish on us at any time (and that's okay!)
233 # But, since the file is gone, we want the entire fd
234 # entry gone too:
235 pass # del inodes[inode]
236
237
238def checkListenersProc():
239 terminalWidth = run(TERMINAL_WIDTH)
240
241 procListeners()
242 addProcessNamesToInodes()
243 tried = inodes
244
245 try:
246 cols = readOutput(terminalWidth)[0]
247 cols = int(cols)
248 except BaseException:
249 cols = 80
250
251 # Print our own custom output header...
252 proto = "Proto"
253 addr = "Listening"
254 pid = "PID"
255 process = "Process"
256 print(f"{COLOR_HEADER}{proto:^5} {addr:^25} {pid:>5} {process:^30}")
257
258 # Could sort by anything: ip, port, proto, pid, command name
259 # (or even the fd integer if that provided any insight whatsoever)
260 def compareByPidOrPort(what):
261 k, v = what
262 # v = [ip, port, proto, pid, cmd]
263 # - OR -
264 # v = [ip, port, proto]
265
266 # If we're not running as root we can't pid and command mappings for
267 # the processes of other users, so sort the pids we did find at end
268 # of list and show UNKNOWN entries first
269 # (because the lines will be shorter most likely so the bigger visual
270 # weight should be lower in the display table)
271 try:
272 # Pid available! Sort by first pid, subsort by IP then port.
273 return (1, v[3], v[0], v[1])
274 except BaseException:
275 # No pid available! Sort by port number then IP then... port again.
276 return (0, v[1], v[0], v[1])
277
278 # Sort results by pid...
279 for name, vals in sorted(tried.items(), key=compareByPidOrPort):
280 attachedPids = vals[3:]
281 if attachedPids:
282 desc = [f"{pid:5} {' '.join(cmd)}" for pid, cmd in vals[3:]]
283 else:
284 # If not running as root, we won't have pid or process, so use
285 # defaults
286 desc = ["UNKNOWN (must be root for global pid mappings)"]
287
288 port = vals[1]
289 try:
290 # Convert port integer to service name if possible
291 port = socket.getservbyport(port)
292 except BaseException:
293 # If no match, just use port number directly.
294 pass
295
296 addr = f"{vals[0]}:{port}"
297 proto = vals[2]
298
299 # If IP address looks like it could be visible to the world,
300 # throw up a color.
301 # Note: due to port forwarding and NAT and other issues,
302 # this clearly isn't exhaustive.
303 if re.match(likelyLocalOnly, addr):
304 colorNotice = COLOR_OKAY
305 else:
306 colorNotice = COLOR_WARNING
307
308 isFirstLine = True
309 for line in desc:
310 if isFirstLine:
311 output = f"{colorNotice}{proto:5} {addr:25} {line}"
312 isFirstLine = False
313 else:
314 output = f"{' ':31} {line}"
315
316 # Be a polite terminal citizen by limiting our width to user's width
317 # (colors take up non-visible space, so add it to our col count)
318 print(output[:cols + (len(colorNotice) if isFirstLine else 0)])
319
320 if ONLY_LOWEST_PID:
321 break
322
323 print(COLOR_END)
324
325
326if __name__ == "__main__":
327 # cheap hack garbage way of setting one option
328 # if we need more options, obviously pull in argparse
329 if len(sys.argv) > 1:
330 ONLY_LOWEST_PID = True
331 else:
332 ONLY_LOWEST_PID = False
333
334 checkListenersProc()
diff --git a/ansible/roles/common/files/ssh-transfer-only.sh b/ansible/roles/common/files/ssh-transfer-only.sh
new file mode 100755
index 0000000..c1f0624
--- /dev/null
+++ b/ansible/roles/common/files/ssh-transfer-only.sh
@@ -0,0 +1,11 @@
1#!/usr/bin/env bash
2
3# Only allow ssh commands starting with 'scp' or 'rsync'
4case $SSH_ORIGINAL_COMMAND in
5 scp*)
6 $SSH_ORIGINAL_COMMAND ;;
7 rsync*)
8 $SSH_ORIGINAL_COMMAND ;;
9 *)
10 echo "Not allowed with this key: $SSH_ORIGINAL_COMMAND" ;;
11esac
diff --git a/ansible/roles/common/files/vimrc.local b/ansible/roles/common/files/vimrc.local
new file mode 100644
index 0000000..378d91e
--- /dev/null
+++ b/ansible/roles/common/files/vimrc.local
@@ -0,0 +1,25 @@
1set encoding=utf-8
2
3set ignorecase
4set smartcase
5
6set title
7
8set backupdir=~/.vim-tmp,/tmp
9set directory=~/.vim-tmp,/tmp
10
11set ruler
12
13filetype plugin indent on
14
15set ai
16set expandtab
17set tabstop=4
18set shiftwidth=4
19
20autocmd BufEnter * :syntax sync fromstart
21
22set hlsearch
23colorscheme peachpuff
24
25set wildignore+=*/tmp/*,*.so,*.swp,*.zip,*.o,*.dSYM,tags
diff --git a/ansible/roles/common/handlers/main.yml b/ansible/roles/common/handlers/main.yml
new file mode 100644
index 0000000..ade4fea
--- /dev/null
+++ b/ansible/roles/common/handlers/main.yml
@@ -0,0 +1,20 @@
1---
2- name: reload sshd
3 service:
4 name: sshd
5 state: reloaded
6
7- name: reload grub
8 command: update-grub
9
10- name: double disable systemd ntp client
11 command: timedatectl set-ntp false
12
13- name: clear motd cache
14 file:
15 path: "{{ item }}"
16 state: absent
17 loop:
18 - /var/cache/motd-news
19 - /run/motd.dynamic
20 - /run/motd.dynamic.new
diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml
new file mode 100644
index 0000000..23de53c
--- /dev/null
+++ b/ansible/roles/common/tasks/main.yml
@@ -0,0 +1,301 @@
1---
2# You can manually view how your OS-provided packages are supported with:
3# ubuntu-support-status --show-all
4- name: update packages
5 apt:
6 update_cache: yes
7 upgrade: safe
8 cache_valid_time: 3600
9
10
11- name: fix inputrc
12 copy:
13 src: inputrc
14 dest: /etc/inputrc
15 owner: root
16 group: root
17 mode: 0644
18
19- name: fix vimrc
20 copy:
21 src: vimrc.local
22 dest: /etc/vim/
23 owner: root
24 group: root
25 mode: 0644
26
27
28- include_role:
29 name: ramdisk
30
31
32- name: remove ubuntu call home reporting cron
33 cron:
34 cron_file: popularity-contest
35 state: absent
36
37
38- name: remove low port restriction
39 sysctl:
40 name: net.ipv4.ip_unprivileged_port_start
41 value: 0
42 state: present
43 sysctl_set: yes
44
45
46# 3 means enable for outgoing and incoming connections
47# 2 means enable for incoming connections
48# 1 means enable for outgoing connections
49# 0 means disabled
50# Linux 3.13 (2014-01-19) and newer
51- name: enable server and client TCP_FASTOPEN
52 sysctl:
53 name: net.ipv4.tcp_fastopen
54 value: 3
55 state: present
56 sysctl_set: yes
57
58
59# These were taken from:
60# https://wiki.mozilla.org/Security/Server_Side_TLS#Pre-defined_DHE_groups
61- name: populate known-good dhparams
62 copy:
63 src: "{{ item }}"
64 dest: "/etc/ssl/{{ item }}"
65 loop:
66 - ffdhe2048.pem
67 - ffdhe3072.pem
68 - ffdhe4096.pem
69
70
71- name: configure /etc/hostname
72 hostname:
73 name: "{{ inventory_hostname }}"
74
75 #- name: Add IP address of all hosts to all hosts
76 # lineinfile:
77 # state: present
78 # dest: /etc/hosts
79 # regexp: '.*{{ item }}$'
80 # line: "{{ hostvars[item].ansible_default_ipv4.address }} {{item}}"
81 # when: hostvars[item].ansible_default_ipv4.address is defined
82 # with_items: "{{ groups['all'] }}"
83
84
85- name: configure sshd to only listen on IPv4
86 lineinfile:
87 dest: /etc/ssh/sshd_config
88 regexp: '^#?AddressFamily'
89 line: "AddressFamily inet" # no ipv6
90 state: present
91 notify: reload sshd
92
93
94 # Capture example:
95 #- replace:
96 # path: /etc/hosts
97 # regexp: '(\s+)old\.host\.name(\s+.*)?$'
98 # replace: '\1new.host.name\2'
99 # backup: yes
100
101
102- name: fix motd
103 replace:
104 path: /etc/default/motd-news
105 regexp: 'https://motd.ubuntu.com'
106 replace: 'https://matt.sh/motd'
107 notify:
108 - clear motd cache
109
110
111# Verify against:
112# systemctl list-timers
113- name: disable more automated call home reporting
114 systemd:
115 name: "{{ item }}"
116 state: stopped
117 enabled: False
118 loop:
119 - apt-daily-upgrade.timer
120 - apt-daily.timer
121 - motd-news.timer
122
123
124- name: remove ubuntu self-advertising
125 file:
126 path: "/etc/update-motd.d/{{ item }}"
127 state: absent
128 loop:
129 - 91-release-upgrade
130 - 80-livepatch
131 - 10-help-text
132 notify:
133 - clear motd cache
134
135
136# Ubuntu's pam_motd.so shows you /etc/legal
137# on login if you don't have ~/.cache/motd.legal-displayed
138# There is no way to disable the creation of that file in ~/.cache on login,
139# but we can wipe out the message for new users.
140- name: remove login disclaimer
141 file:
142 path: /etc/legal
143 state: absent
144
145
146- name: place net-listeners.py
147 copy:
148 src: net-listeners.py
149 dest: /usr/local/bin/
150 owner: root
151 group: root
152 mode: 0755
153
154- name: place scp/rsync-only ssh restriction capability
155 copy:
156 src: ssh-transfer-only.sh
157 dest: /usr/local/bin/
158 owner: root
159 group: root
160 mode: 0755
161
162# can't setsid 04755 scripts, so enable script with global passwordless sudo
163- name: enable all user running of net-listeners.py
164 lineinfile:
165 path: /etc/sudoers.d/net-listeners
166 regexp: "listeners.py"
167 line: "ALL ALL = (root) NOPASSWD: /usr/local/bin/net-listeners.py"
168 create: yes
169 mode: 0440
170
171- name: add uptime and uname to login motd
172 lineinfile:
173 dest: /etc/update-motd.d/00-header
174 line: "{{ item }}"
175 state: present
176 loop:
177 - printf "\n$(w -us)\n"
178
179- name: add listening watcher to global login config
180 lineinfile:
181 dest: /etc/bash.bashrc
182 line: "{{ item }}"
183 state: present
184 loop:
185 # Only show output when running a login, not when starting a sudo shell
186 - "[[ -z $SUDO_UID ]] && sudo /usr/local/bin/net-listeners.py"
187
188- name: ensure system grub template has serial access
189 lineinfile:
190 dest: /etc/default/grub
191 regexp: '^GRUB_CMDLINE_LINUX='
192 line: 'GRUB_CMDLINE_LINUX="console=ttyS0 {{ grub.extras }}"'
193 state: present
194 notify: reload grub
195
196
197# This is an ops opinion. For more advanced needs, modify here or just template
198# the entire sshd_config directly.
199- name: configure sshd to only listen on local IP
200 lineinfile:
201 dest: /etc/ssh/sshd_config
202 regexp: '^#?ListenAddress'
203 line: "ListenAddress {{ hostvars[inventory_hostname]['ansible_' + network.interface.private]['ipv4']['address'] }}"
204 state: present
205 notify: reload sshd
206
207
208- name: install system tools
209 apt:
210 pkg:
211 # acl is required for ansible to "become_user" as someone non-root because
212 # of permissions on its temporary files. Ansible will setfacl on temp files
213 # so it doesn't have to 0666 everything just so a new user can modify things.
214 - acl
215
216 # you aren't a linux server without sending nightly summary emails
217 - logwatch
218
219 # apt helpers for repo installs not included by default for some reason
220 - software-properties-common
221
222 # production CA bundles so we don't get unknown CA errors
223 - ca-certificates
224
225 # Maintains high numbers in /proc/sys/kernel/random/entropy_avail
226 - rng-tools
227
228 # should we use a more modern thing than collect? distributed osquery?
229 - collectd
230
231 # make sure 'install_recommends: no' or this installs lots of other stuff
232 - vim-nox
233
234 # rrdtool only installed so we can be lazy and generate graphs on-demand
235 # with: /usr/share/doc/collectd-core/examples/collectd2html.pl
236 # TODO: enable centralized reporting system
237 - rrdtool
238
239 # netstat, mii-tool, etc
240 - net-tools
241 install_recommends: no
242 state: latest
243
244# use a modern ntp client+server.
245#
246# systemd actually has a built-in ntp client called 'systemd-timesyncd'
247# You can view its status with:
248# journalctl -u systemd-timesyncd
249# timedatectl
250#
251# Installing chrony will disable systemd-timesyncd
252# (represented in apt with "Replaces: time-daemon")
253# but it doesn't _actually_ disable it according to timedatectl (bug?)
254# so we also manually run 'timedatectl set-ntp false' just to confirm.
255# A good writeup about systemd-timesyncd lives at:
256# https://wiki.archlinux.org/index.php/systemd-timesyncd
257#
258# You can view your live chrony status with:
259# chronyc tracking
260# chronyc sources
261# chronyc sourcestats
262#
263# ...and that's a lot more detail than the built-in garabage systemd-timesyncd
264# client will tell you about how your system time is being managed.
265#
266# chrony is both an ntp client with a remote administration interface
267# and an ntp server, but by default chrony does not enable remote admin
268# or ntp serving without additional explicit configuration (chrony.conf).
269#
270# For more details about becoming an ntp server and remote time administartion,
271# see sections 2.2 and 2.5 of:
272# https://chrony.tuxfamily.org/faq.html#_how_do_i_make_an_ntp_server_from_an_ntp_client
273- name: install ntp client
274 apt:
275 pkg: chrony
276 state: latest
277 notify:
278 - double disable systemd ntp client
279
280# If ansible facts aren't enough, we can get puppet and chef facts too:
281#- name: install facter
282# apt:
283# pkg: facter
284# state: latest
285#
286#- name: install ohai
287# apt:
288# pkg: facter
289# state: latest
290
291
292# cleanup
293- name: cleanup packaging
294 apt:
295 autoclean: yes
296 autoremove: yes
297
298# If needed, build and provide:
299#
300# Build for nsjail:
301# apt install protobu* bison flex pkg-config libprotobuf-dev
diff --git a/ansible/roles/disableFirewall/files/modprobe.d/blacklist-iptables.conf b/ansible/roles/disableFirewall/files/modprobe.d/blacklist-iptables.conf
new file mode 100644
index 0000000..4655374
--- /dev/null
+++ b/ansible/roles/disableFirewall/files/modprobe.d/blacklist-iptables.conf
@@ -0,0 +1,13 @@
1# Don't load iptables on startup (or ever)!
2
3# These look weird, but the 'blacklist' command still allows
4# module insertion.
5#
6# This method defines a load-time alias so when you load the module,
7# it runs a delegated command to load the module instead, but in
8# the case of denying modules completely, just run nothing.
9install ip6table_filter /bin/true
10install iptable_filter /bin/true
11install ip6_tables /bin/true
12install ip_tables /bin/true
13install x_tables /bin/true
diff --git a/ansible/roles/disableFirewall/tasks/main.yml b/ansible/roles/disableFirewall/tasks/main.yml
new file mode 100644
index 0000000..9454702
--- /dev/null
+++ b/ansible/roles/disableFirewall/tasks/main.yml
@@ -0,0 +1,22 @@
1---
2# Our mail systems only listen to SMTP(S) and IMAP(S)
3# so we can disable all firewalls
4# This stops ufw, then uninstalls ufw and iptables (and ip6tables)
5- name: remove firewall
6 apt:
7 name: iptables
8 state: absent
9 register: firewallKaboom
10
11# removing iptables doesn't actually stop iptables processing,
12# so let's force remove all packet processing from the kernel itself here
13# TODO: this conditional could be better. would be nice if we had a fact
14# of loaded kernel modules to query the presence/absence of
15- name: unload firewall
16 command: modprobe -r ip6table_filter iptable_filter ip6_tables ip_tables x_tables
17 when: firewallKaboom.changed
18
19- name: disable iptables from reappearing in the future
20 copy:
21 src: modprobe.d/
22 dest: /etc/modprobe.d/
diff --git a/ansible/roles/dovecot/files/dovecot/authdb.sqlite3.empty b/ansible/roles/dovecot/files/dovecot/authdb.sqlite3.empty
new file mode 100644
index 0000000..f1e0330
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/authdb.sqlite3.empty
Binary files differ
diff --git a/ansible/roles/dovecot/files/dovecot/conf.d/10-acl.conf b/ansible/roles/dovecot/files/dovecot/conf.d/10-acl.conf
new file mode 100644
index 0000000..f9fa335
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/conf.d/10-acl.conf
@@ -0,0 +1,15 @@
1mail_plugins = $mail_plugins acl
2
3protocol imap {
4 mail_plugins = $mail_plugins imap_acl
5}
6
7plugin {
8 acl_defaults_from_inbox = yes
9}
10
11# Should saving a mail to a nonexistent mailbox automatically create it?
12lda_mailbox_autocreate = yes
13
14# Should automatically created mailboxes be also automatically subscribed?
15lda_mailbox_autosubscribe = yes
diff --git a/ansible/roles/dovecot/files/dovecot/conf.d/10-auth.conf b/ansible/roles/dovecot/files/dovecot/conf.d/10-auth.conf
new file mode 100644
index 0000000..4e23fa8
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/conf.d/10-auth.conf
@@ -0,0 +1,46 @@
1# cache all authentication results for one hour
2#auth_cache_size = 10M
3#auth_cache_ttl = 1 hour
4#auth_cache_negative_ttl = 1 hour
5
6# Don't cache password details, otherwise password changes require a server HUP
7# before the server will re-query the password data source.
8auth_cache_size = 0
9
10# only use plain username/password auth - OK since everything is over TLS
11auth_mechanisms = plain
12
13# passdb specifies how users are authenticated - sql here, and
14# our sql config specifies the sqlite filename with queries to use
15passdb {
16 driver = sql
17 args = /etc/dovecot/dovecot-sql.conf.ext
18}
19
20# userdb specifies the location of users' "home" directories - where their
21# mail is stored. e.g. /var/mail/vhosts/exmaple.com/user
22# %d = domain, %n = user
23# We can't use "prefetch" because postfix can't read users from "prefetch" db,
24# and we can't use 'static' because the doveadm tool needs to iterate users
25# for purging zero refcount deleted mails, so we give a userdb of sql here
26# and specify a "get all users" SQL query in the configuration file.
27userdb {
28 driver = sql
29 args = /etc/dovecot/dovecot-sql.conf.ext
30}
31
32# UNIX socket path to master authentication server to find users.
33# This is used by imap (for shared users) and lda.
34auth_socket_path = /var/run/dovecot/auth-userdb
35
36# Respect /etc/hosts.deny (populated by fail2ban)
37# You can use /etc/hosts.allow to countermand fail2ban decisions.
38login_access_sockets = tcpwrap
39
40service tcpwrap {
41 unix_listener login/tcpwrap {
42 group = $default_login_user
43 mode = 0600
44 user = $default_login_user
45 }
46}
diff --git a/ansible/roles/dovecot/files/dovecot/conf.d/10-mail.conf b/ansible/roles/dovecot/files/dovecot/conf.d/10-mail.conf
new file mode 100644
index 0000000..6c03965
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/conf.d/10-mail.conf
@@ -0,0 +1,28 @@
1# default home directory location for all users
2mail_home = /var/mail/vhosts/%d/%n
3
4# directory to store mail. The tilda makes it relative to the *dovecot*
5# virtual home directory.
6#
7# I use mdbox - this is Dovecot's own high-performance mail store format.
8# There are other slower, more "traditional" formats you can choose from.
9# Read about them here: https://wiki2.dovecot.org/MailboxFormat
10mail_location = mdbox:~/mdbox
11
12# nothing fancy - just a standard default namespace with '/' as the
13# hierarchy separator
14namespace inbox {
15 separator = /
16 inbox = yes
17}
18
19# set this to the group that owns your vmail directory.
20mail_privileged_group = vmail
21
22# these lines enable attachment deduplication. Attachments must be somewhat
23# large (64k) to store them separately from the mail store.
24mail_attachment_dir = /var/mail/attachments
25mail_attachment_min_size = 64k
26
27# we'll uncomment this after we set up Solr in the following section:
28# mail_plugins = $mail_plugins fts fts_solr
diff --git a/ansible/roles/dovecot/files/dovecot/conf.d/10-master.conf b/ansible/roles/dovecot/files/dovecot/conf.d/10-master.conf
new file mode 100644
index 0000000..f99d0f4
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/conf.d/10-master.conf
@@ -0,0 +1,82 @@
1# to improve performance, disable fsync globally - we will enable it for
2# some specific services later on
3mail_fsync = never
4
5service imap-login {
6 # plain-text IMAP should only be accessible from localhost
7 inet_listener imap {
8 address = 127.0.0.1, ::1
9 }
10
11 # enable high-performance mode, described here:
12 # https://wiki.dovecot.org/LoginProcess
13 service_count = 0
14
15 # set to the number of CPU cores on your server
16 process_min_avail = 3
17 vsz_limit = 1G
18}
19
20# disable POP3 altogether
21service pop3-login {
22 inet_listener pop3 {
23 port = 0
24 }
25
26 inet_listener pop3s {
27 port = 0
28 }
29}
30
31# enable semi-long-lived IMAP processes to improve performance
32service imap {
33 service_count = 256
34 # set to the number of CPU cores on your server
35 process_min_avail = 3
36}
37
38# expose an LMTP socket for postfix to deliver mail
39service lmtp {
40 unix_listener /var/spool/postfix/private/dovecot-lmtp {
41 group = postfix
42 mode = 0600
43 user = postfix
44 }
45}
46
47service auth {
48 # auth_socket_path points to this userdb socket by default. It's typically
49 # used by dovecot-lda, doveadm, possibly imap process, etc. Users that have
50 # full permissions to this socket are able to get a list of all usernames and
51 # get the results of everyone's userdb lookups.
52 #
53 # The default 0666 mode allows anyone to connect to the socket, but the
54 # userdb lookups will succeed only if the userdb returns an "uid" field that
55 # matches the caller process's UID. Also if caller's uid or gid matches the
56 # socket's uid or gid the lookup succeeds. Anything else causes a failure.
57 #
58 # To give the caller full permissions to lookup all users, set the mode to
59 # something else than 0666 and Dovecot lets the kernel enforce the
60 # permissions (e.g. 0777 allows everyone full permissions).
61
62 # auth for postfix
63 unix_listener /var/spool/postfix/private/auth {
64 mode = 0666
65 user = postfix
66 group = postfix
67 }
68
69 # auth for doveadm tools
70 unix_listener auth-userdb {
71 mode = 0666
72 user = vmail
73 group = vmail
74 }
75
76 client_limit = 840
77}
78
79# no need to run this as root
80service auth-worker {
81 user = vmail
82}
diff --git a/ansible/roles/dovecot/files/dovecot/conf.d/15-lda.conf b/ansible/roles/dovecot/files/dovecot/conf.d/15-lda.conf
new file mode 100644
index 0000000..32ca50d
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/conf.d/15-lda.conf
@@ -0,0 +1,7 @@
1# configuration for mail delivered by the `dovecot-lda` command. Shouldn't
2# be needed since we are using LMTP, but kept for backwards compatibility.
3protocol lda {
4 # use fsync for write-safety - this deals with delivering actual mail
5 mail_fsync = optimized
6 mail_plugins = $mail_plugins sieve
7}
diff --git a/ansible/roles/dovecot/files/dovecot/conf.d/15-mailboxes.conf b/ansible/roles/dovecot/files/dovecot/conf.d/15-mailboxes.conf
new file mode 100644
index 0000000..8674b0b
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/conf.d/15-mailboxes.conf
@@ -0,0 +1,24 @@
1# define any special IMAP folders here. You can force them to be created or
2# created+subscribed automatically used the `auto` option.
3namespace inbox {
4 mailbox Drafts {
5 auto = subscribe
6 special_use = \Drafts
7 }
8 mailbox Junk {
9 auto = create
10 special_use = \Junk
11 }
12 mailbox Trash {
13 auto = create
14 special_use = \Trash
15 }
16 mailbox Archive {
17 auto = subscribe
18 special_use = \Archive
19 }
20 mailbox Sent {
21 auto = subscribe
22 special_use = \Sent
23 }
24}
diff --git a/ansible/roles/dovecot/files/dovecot/conf.d/20-imap.conf b/ansible/roles/dovecot/files/dovecot/conf.d/20-imap.conf
new file mode 100644
index 0000000..7b32396
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/conf.d/20-imap.conf
@@ -0,0 +1,10 @@
1# Use a longer IDLE interval to reduce network chatter and save battery
2# life. Max is 30 minutes.
3imap_idle_notify_interval = 29 mins
4
5protocol imap {
6 # max IMAP connections per IP address
7 mail_max_userip_connections = 50
8 # imap_sieve will be used for spam training by rspamd
9 mail_plugins = $mail_plugins imap_sieve
10}
diff --git a/ansible/roles/dovecot/files/dovecot/conf.d/20-lmtp.conf b/ansible/roles/dovecot/files/dovecot/conf.d/20-lmtp.conf
new file mode 100644
index 0000000..a51ee42
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/conf.d/20-lmtp.conf
@@ -0,0 +1,5 @@
1protocol lmtp {
2 # use fsync for write-safety - this deals with delivering actual mail
3 mail_fsync = optimized
4 mail_plugins = $mail_plugins sieve
5}
diff --git a/ansible/roles/dovecot/files/dovecot/conf.d/90-imapsieve.conf b/ansible/roles/dovecot/files/dovecot/conf.d/90-imapsieve.conf
new file mode 100644
index 0000000..26987b1
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/conf.d/90-imapsieve.conf
@@ -0,0 +1,18 @@
1plugin {
2 sieve_plugins = sieve_imapsieve sieve_extprograms
3
4 # From elsewhere to Junk folder
5 imapsieve_mailbox1_name = Junk
6 imapsieve_mailbox1_causes = COPY
7 imapsieve_mailbox1_before = file:/etc/dovecot/sieve/report-spam.sieve
8
9 # From Junk folder to elsewhere
10 imapsieve_mailbox2_name = *
11 imapsieve_mailbox2_from = Junk
12 imapsieve_mailbox2_causes = COPY
13 imapsieve_mailbox2_before = file:/etc/dovecot/sieve/report-ham.sieve
14
15 sieve_pipe_bin_dir = /etc/dovecot/sieve
16
17 sieve_global_extensions = +vnd.dovecot.pipe
18}
diff --git a/ansible/roles/dovecot/files/dovecot/conf.d/90-sieve.conf b/ansible/roles/dovecot/files/dovecot/conf.d/90-sieve.conf
new file mode 100644
index 0000000..9a753bf
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/conf.d/90-sieve.conf
@@ -0,0 +1,34 @@
1plugin {
2 # 'active' is a symlink to one sieve source script inside directory at 'file'
3 sieve = file:~/sieve;active=~/.dovecot.sieve
4
5 # directory of global sieve scripts to run before and after processing ALL
6 # incoming mail
7 sieve_before = /etc/dovecot/sieve-before.d
8 sieve_after = /etc/dovecot/sieve-after.d
9
10 # make sieve aware of [email protected] aliases
11 recipient_delimiter = +
12
13
14 # no limits on script size or actions
15 sieve_quota_max_storage = 0
16 sieve_max_script_size = 0
17 sieve_max_actions = 0
18
19 sieve_extensions = +spamtest +spamtestplus
20
21 sieve_spamtest_status_header = X-Spam-Score
22 sieve_spamtest_status_type = strlen
23
24 # X-Spamd-Bar: +++++++++
25 sieve_spamtest_max_value = 9
26
27
28 # X-Spamd-Result: default: False [9.19 / 15.00];
29 # (regex not fixed to capture the above)
30 #sieve_spamtest_status_type = score
31 #sieve_spamtest_status_header = \
32 # X-Spamd-Result: [[:alnum:]]+, score=(-?[[:digit:]]+\.[[:digit:]])
33 #sieve_spamtest_max_value = 5.0
34}
diff --git a/ansible/roles/dovecot/files/dovecot/dovecot-sql.conf.ext b/ansible/roles/dovecot/files/dovecot/dovecot-sql.conf.ext
new file mode 100644
index 0000000..f248243
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/dovecot-sql.conf.ext
@@ -0,0 +1,150 @@
1# This file is commonly accessed via passdb {} or userdb {} section in
2# conf.d/auth-sql.conf.ext
3
4# This file is opened as root, so it should be owned by root and mode 0600.
5#
6# http://wiki2.dovecot.org/AuthDatabase/SQL
7#
8# For the sql passdb module, you'll need a database with a table that
9# contains fields for at least the username and password. If you want to
10# use the user@domain syntax, you might want to have a separate domain
11# field as well.
12#
13# If your users all have the same uig/gid, and have predictable home
14# directories, you can use the static userdb module to generate the home
15# dir based on the username and domain. In this case, you won't need fields
16# for home, uid, or gid in the database.
17#
18# If you prefer to use the sql userdb module, you'll want to add fields
19# for home, uid, and gid. Here is an example table:
20#
21# CREATE TABLE users (
22# username VARCHAR(128) NOT NULL,
23# domain VARCHAR(128) NOT NULL,
24# password VARCHAR(64) NOT NULL,
25# home VARCHAR(255) NOT NULL,
26# uid INTEGER NOT NULL,
27# gid INTEGER NOT NULL,
28# active CHAR(1) DEFAULT 'Y' NOT NULL
29# );
30
31# Database driver: mysql, pgsql, sqlite
32driver = sqlite
33
34# Database connection string. This is driver-specific setting.
35#
36# HA / round-robin load-balancing is supported by giving multiple host
37# settings, like: host=sql1.host.org host=sql2.host.org
38#
39# pgsql:
40# For available options, see the PostgreSQL documention for the
41# PQconnectdb function of libpq.
42# Use maxconns=n (default 5) to change how many connections Dovecot can
43# create to pgsql.
44#
45# mysql:
46# Basic options emulate PostgreSQL option names:
47# host, port, user, password, dbname
48#
49# But also adds some new settings:
50# client_flags - See MySQL manual
51# connect_timeout - Connect timeout in seconds (default: 5)
52# read_timeout - Read timeout in seconds (default: 30)
53# write_timeout - Write timeout in seconds (default: 30)
54# ssl_ca, ssl_ca_path - Set either one or both to enable SSL
55# ssl_cert, ssl_key - For sending client-side certificates to server
56# ssl_cipher - Set minimum allowed cipher security (default: HIGH)
57# ssl_verify_server_cert - Verify that the name in the server SSL certificate
58# matches the host (default: no)
59# option_file - Read options from the given file instead of
60# the default my.cnf location
61# option_group - Read options from the given group (default: client)
62#
63# You can connect to UNIX sockets by using host: host=/var/run/mysql.sock
64# Note that currently you can't use spaces in parameters.
65#
66# sqlite:
67# The path to the database file.
68#
69# Examples:
70# connect = host=192.168.1.1 dbname=users
71# connect = host=sql.example.com dbname=virtual user=virtual password=blarg
72# connect = /etc/dovecot/authdb.sqlite
73#
74connect = /etc/dovecot/authdb.sqlite
75
76# Default password scheme.
77#
78# List of supported schemes is in
79# http://wiki2.dovecot.org/Authentication/PasswordSchemes
80#
81#default_pass_scheme = SHA512-CRYPT
82
83# passdb query to retrieve the password. It can return fields:
84# password - The user's password. This field must be returned.
85# user - user@domain from the database. Needed with case-insensitive lookups.
86# username and domain - An alternative way to represent the "user" field.
87#
88# The "user" field is often necessary with case-insensitive lookups to avoid
89# e.g. "name" and "nAme" logins creating two different mail directories. If
90# your user and domain names are in separate fields, you can return "username"
91# and "domain" fields instead of "user".
92#
93# The query can also return other fields which have a special meaning, see
94# http://wiki2.dovecot.org/PasswordDatabase/ExtraFields
95#
96# Commonly used available substitutions (see http://wiki2.dovecot.org/Variables
97# for full list):
98# %u = entire user@domain
99# %n = user part of user@domain
100# %d = domain part of user@domain
101#
102# Note that these can be used only as input to SQL query. If the query outputs
103# any of these substitutions, they're not touched. Otherwise it would be
104# difficult to have eg. usernames containing '%' characters.
105#
106# Example:
107# password_query = SELECT userid AS user, pw AS password \
108# FROM users WHERE userid = '%u' AND active = 'Y'
109#
110password_query = \
111 SELECT '%u' AS username, domain, password \
112 FROM users WHERE userid = '%n' AND domain = '%d'
113
114# You can update (or modify this a bit to insert) user passwords in a shell with:
115# sqlite3 authdb.sqlite "update users set password='$(doveadm pw -s SHA512-CRYPT -r 1856250)' where userid='USERNAME' and domain = 'DOMAIN';"
116
117
118# userdb query to retrieve the user information. It can return fields:
119# uid - System UID (overrides mail_uid setting)
120# gid - System GID (overrides mail_gid setting)
121# home - Home directory
122# mail - Mail location (overrides mail_location setting)
123#
124# None of these are strictly required. If you use a single UID and GID, and
125# home or mail directory fits to a template string, you could use userdb static
126# instead. For a list of all fields that can be returned, see
127# http://wiki2.dovecot.org/UserDatabase/ExtraFields
128#
129# Examples:
130# user_query = SELECT home, uid, gid FROM users WHERE userid = '%u'
131# user_query = SELECT dir AS home, user AS uid, group AS gid FROM users where userid = '%u'
132# user_query = SELECT home, 501 AS uid, 501 AS gid FROM users WHERE userid = '%u'
133#
134user_query = \
135 SELECT "/var/mail/vhosts/" || '%d' || '/' || '%n' AS home, 145 as uid, 145 as gid
136
137# If you wish to avoid two SQL lookups (passdb + userdb), you can use
138# userdb prefetch instead of userdb sql in dovecot.conf. In that case you'll
139# also have to return userdb fields in password_query prefixed with "userdb_"
140# string. For example:
141password_query = \
142 SELECT '%u' AS user, password, \
143 "/var/mail/vhosts/" || '%d' || '/' || '%n' AS userdb_home, 145 AS userdb_uid, 145 AS userdb_gid \
144 FROM users WHERE userid = '%n' AND domain = '%d'
145
146# Query to get a list of all usernames.
147# This iteration is used for things like globally purging zero refcount emails
148# for all users, but to get all users, we have to iterate the user storage,
149# hence this iterator query is required.
150iterate_query = SELECT userid AS user FROM users
diff --git a/ansible/roles/dovecot/files/dovecot/dovecot.conf b/ansible/roles/dovecot/files/dovecot/dovecot.conf
new file mode 100644
index 0000000..4304fa6
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/dovecot.conf
@@ -0,0 +1,19 @@
1# IMAP for remote access, LMTP for local delivery
2protocols = imap lmtp
3
4# set these to the uid of your `vmail` user
5first_valid_uid = 145
6last_valid_uid = 145
7
8#mail_debug = yes
9##auth_verbose = yes
10##auth_debug = yes
11##auth_debug_passwords = yes
12##auth_verbose_passwords = yes
13
14
15mail_uid = vmail
16mail_gid = vmail
17
18!include conf.d/*.conf
19!include_try local.conf
diff --git a/ansible/roles/dovecot/files/dovecot/sieve-before.d/10-rspamd.sieve b/ansible/roles/dovecot/files/dovecot/sieve-before.d/10-rspamd.sieve
new file mode 100644
index 0000000..7931a71
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/sieve-before.d/10-rspamd.sieve
@@ -0,0 +1,5 @@
1require ["fileinto"];
2
3if header :is "X-Spam" "Yes" {
4 fileinto "Junk";
5}
diff --git a/ansible/roles/dovecot/files/dovecot/sieve/report-ham.sieve b/ansible/roles/dovecot/files/dovecot/sieve/report-ham.sieve
new file mode 100644
index 0000000..2ad40aa
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/sieve/report-ham.sieve
@@ -0,0 +1,23 @@
1require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
2
3if environment :matches "imap.mailbox" "*" {
4 set "mailbox" "${1}";
5}
6
7# This line is important because when we delete from Junk/Spam,
8# messages get moved to Trash, which tirggers the "message moved out of
9# spam" script (this script) which—usually!—trains the originally classified
10# Spam as not-spam.
11# BUT, this is just a delete! If we train our spam as not-spam on delete, that
12# defeats our goals.
13# In short, this always gets run on a message being moved out of Spam, but if
14# the target mailbox is Trash, just don't run the trainer this time.
15if string "${mailbox}" "Trash" {
16 stop;
17}
18
19if environment :matches "imap.email" "*" {
20 set "email" "${1}";
21}
22
23pipe :copy "train-ham.sh" [ "${email}" ];
diff --git a/ansible/roles/dovecot/files/dovecot/sieve/report-spam.sieve b/ansible/roles/dovecot/files/dovecot/sieve/report-spam.sieve
new file mode 100644
index 0000000..4ed95d7
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/sieve/report-spam.sieve
@@ -0,0 +1,7 @@
1require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
2
3if environment :matches "imap.email" "*" {
4 set "email" "${1}";
5}
6
7pipe :copy "train-spam.sh" [ "${email}" ];
diff --git a/ansible/roles/dovecot/files/dovecot/sieve/train-ham.sh b/ansible/roles/dovecot/files/dovecot/sieve/train-ham.sh
new file mode 100755
index 0000000..b0f30fd
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/sieve/train-ham.sh
@@ -0,0 +1 @@
exec /usr/bin/rspamc --connect localhost:11334 learn_ham
diff --git a/ansible/roles/dovecot/files/dovecot/sieve/train-spam.sh b/ansible/roles/dovecot/files/dovecot/sieve/train-spam.sh
new file mode 100755
index 0000000..bf1b920
--- /dev/null
+++ b/ansible/roles/dovecot/files/dovecot/sieve/train-spam.sh
@@ -0,0 +1 @@
exec /usr/bin/rspamc --connect localhost:11334 learn_spam
diff --git a/ansible/roles/dovecot/handlers/main.yml b/ansible/roles/dovecot/handlers/main.yml
new file mode 100644
index 0000000..dda7930
--- /dev/null
+++ b/ansible/roles/dovecot/handlers/main.yml
@@ -0,0 +1,31 @@
1---
2- name: restart dovecot
3 service:
4 name: dovecot
5 state: restarted
6
7- name: reload dovecot
8 service:
9 name: dovecot
10 state: reloaded
11
12# We intentionally don't have a "creates:" guard on the resieve handlers
13# because if they get called we need to re-run them on any changes of
14# the underlying script itself regardless whether the result .svbin
15# already exists or not.
16- name: resieve spam
17 command: sievec report-spam.sieve
18 args:
19 chdir: /etc/dovecot/sieve
20
21- name: resieve ham
22 command: sievec report-ham.sieve
23 args:
24 chdir: /etc/dovecot/sieve
25
26- name: resieve spam mover
27 command: sievec 10-rspamd.sieve
28 args:
29 chdir: /etc/dovecot/sieve-before.d
30 creates: 10-rspamd.svbin
31
diff --git a/ansible/roles/dovecot/tasks/main.yml b/ansible/roles/dovecot/tasks/main.yml
new file mode 100644
index 0000000..6e097c1
--- /dev/null
+++ b/ansible/roles/dovecot/tasks/main.yml
@@ -0,0 +1,110 @@
1---
2# dovecot install and configuration
3- name: install dovecot
4 apt:
5 state: latest
6 pkg:
7 - dovecot-imapd
8 - dovecot-lmtpd
9 - dovecot-sieve
10 - dovecot-sqlite
11
12# Convert existing maildir to mdbox (local on-fs dirs) with:
13# dsync -o mail_location=mdbox:herebox mirror maildir:Maildir
14# Or, you can pull from a remote site:
15# Below, -R means REVERSE backup so PULL messages FROM vorash INTO mdbox,
16# otherwise, it's a PUSH backup and mdbox PUSHES to vorash which isn't what we want
17# doveadm -o mail_location=mdbox:herebox backup -R ssh -J [email protected] matt@vorash doveadm dsync-server
18- name: create mail spool dirs
19 file:
20 path: /var/mail/local
21 owner: root
22 group: mail
23 mode: 0775
24 state: directory
25
26- name: create dovecot virtual mailbox group
27 group:
28 name: vmail
29 gid: 145
30 state: present
31
32- name: create dovecot virtual mailbox and virtual authentication account
33 user:
34 name: vmail
35 uid: 145
36 group: vmail
37 shell: /sbin/nologin
38 create_home: yes
39 home: /var/mail/vhosts
40 state: present
41
42- name: give dovecot user permission to read private keys
43 user:
44 name: dovecot
45 groups: ssl-cert
46 append: yes
47
48# Create new passwords with:
49# time doveadm pw -s SHA512-CRYPT -r 1856250
50- name: copy dovecot configs and userdb
51 copy:
52 src: dovecot/
53 dest: /etc/dovecot/
54 mode: preserve
55 notify:
56 - resieve spam
57 - resieve ham
58 - resieve spam mover
59 - restart dovecot
60
61# This permission is important because dovecot has multiple users:
62# - dovecot
63# - dovenull
64# - vmail
65# but login processes are run by the 'vmail' user, so 'vmail' must have read
66# access to the DB
67- name: fix user permissions on authdb
68 file:
69 path: /etc/dovecot/authdb.sqlite
70 owner: vmail
71 group: vmail
72 mode: 0600
73
74- name: instantiate dovecot SSL template with host vars
75 template:
76 src: dovecot/conf.d/10-ssl.conf.j2
77 dest: /etc/dovecot/conf.d/10-ssl.conf
78 notify:
79 - restart dovecot # NB this could be a reload instead
80
81# Dovecot mdbox format requires a purge to remove storage space
82# allocated to messages that have been fully deleted by users.
83# (it's an append-only refcounting system, so when a refcount becomes
84# zero on final delete, it needs some cleanup to rewrite the old
85# pack files without the deleted emails present anymore.)
86- cron:
87 name: setup cron so dovecot can GC mailboxes
88 minute: 0
89 hour: 3
90 user: vmail
91 job: "doveadm purge -A"
92 cron_file: dovecot_maint_purge
93
94
95# verify everything is running
96- name: verify services are running in dependency order
97 service:
98 name: "{{ item }}"
99 enabled: yes
100 state: started
101 loop:
102 - dovecot
103
104- name: reload if certs newish
105 include_role:
106 name: certreload
107 vars:
108 certreload:
109 notifiers:
110 - reload dovecot
diff --git a/ansible/roles/dovecot/templates/dovecot/conf.d/10-ssl.conf.j2 b/ansible/roles/dovecot/templates/dovecot/conf.d/10-ssl.conf.j2
new file mode 100644
index 0000000..c1654cd
--- /dev/null
+++ b/ansible/roles/dovecot/templates/dovecot/conf.d/10-ssl.conf.j2
@@ -0,0 +1,22 @@
1# require SSL for all non-localhost connections
2ssl = required
3
4# Config detials at https://wiki.dovecot.org/SSL/DovecotConfiguration
5ssl_cert = </etc/ssl/{{ network.hostname.public }}-cert-combined.rsa2048.pem
6ssl_key = </etc/ssl/private/{{ network.hostname.public }}-key.rsa2048.pem
7
8# Since v2.2.31+ you can specify alternative ssl certificate
9# if the algorithm differs from the primary certificate.
10# This is useful when migrating to e.g. ECDSA certificate.
11ssl_alt_cert = </etc/ssl/{{ network.hostname.public }}-cert-combined.prime256v1.pem
12ssl_alt_key = </etc/ssl/private/{{ network.hostname.public }}-key.prime256v1.pem
13
14# require modern crypto - taken from Mozilla's SSL recommendations page
15ssl_dh_parameters_length = 4096
16ssl_protocols = !SSLv3 !TLSv1 !TLSv1.1 TLSv1.2
17ssl_cipher_list = ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
18ssl_prefer_server_ciphers = yes
19
20# newer dovecot 2.3+
21#ssl_min_protocol = TLSv1.2
22#ssl_dh = /etc/ssl/ffdhe4096.pem
diff --git a/ansible/roles/fail2ban/files/fail2ban/fail2ban.local b/ansible/roles/fail2ban/files/fail2ban/fail2ban.local
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ansible/roles/fail2ban/files/fail2ban/fail2ban.local
diff --git a/ansible/roles/fail2ban/files/fail2ban/filter.d/postfix-rspamd.conf b/ansible/roles/fail2ban/files/fail2ban/filter.d/postfix-rspamd.conf
new file mode 100644
index 0000000..311936b
--- /dev/null
+++ b/ansible/roles/fail2ban/files/fail2ban/filter.d/postfix-rspamd.conf
@@ -0,0 +1,11 @@
1[INCLUDES]
2before = common.conf
3
4[Definition]
5_daemon = postfix/cleanup
6_port = (?::\d+)?
7
8# Line looks like:
9# Jul 4 16:40:22 mailmash postfix/cleanup[14378]: F3FECD008FA: milter-reject: END-OF-MESSAGE from fixed-187-188-96-153.totalplay.net[187.188.96.153]: 5.7.1 Spam message rejected; from=<[email protected]> to=<[email protected]> proto=ESMTP helo=<fixed-187-188-96-153.totalplay.net>
10failregex = milter-reject: END-OF-MESSAGE from [a-z0-9.-]+\[<HOST>\]: 5.7.1 Spam message rejected
11ignoreregex =
diff --git a/ansible/roles/fail2ban/files/fail2ban/jail.local b/ansible/roles/fail2ban/files/fail2ban/jail.local
new file mode 100644
index 0000000..5b77b5b
--- /dev/null
+++ b/ansible/roles/fail2ban/files/fail2ban/jail.local
@@ -0,0 +1,59 @@
1[DEFAULT]
2# For 'banaction' you can use any action defined in /etc/fail2ban/action.d/
3# including things like iptables, iptables-ipset, nftables-*, ...
4banaction = hostsdeny
5banaction_allports = hostsdeny
6
7# Blocking decision making is fully logged in /var/log/fail2ban.log
8# Current blocking can be viewed with:
9# fail2ban-client status
10# fail2ban-client status [service]
11
12# You can unban IPs with
13# fail2ban-client unban <ip>...
14#
15# Or unban just for one service/jail
16# fail2ban-client set <jail> unban <ip>
17
18# Go away for a long time
19bantime = 34d
20
21
22# DEBUGGING
23# You can debug fail2ban behavior by running it in the foreground with
24# client debug and server debug logging:
25# fail2ban-client -vvvvvvvvvv --loglevel DEBUG -f -x start
26# In another terminal:
27# tail -F /var/log/fail2ban.log
28#
29# It helps to delete the persistent save db before fail2ban is started
30# in debug mode too:
31# rm /var/lib/fail2ban/fail2ban.sqlite3
32
33
34# And we're always watching
35# If you're testing/debugging your auth and failing your own logins
36# either by mistake or intentionally, you'll want to either decrease
37# the findtime, decrease the bantime, increase the maxretry time,
38# or just disable fail2ban for [findtime] after your testing.
39findtime = 6h
40
41# Quick and done
42maxretry = 5
43
44[sshd]
45# Disable sshd since we don't have public ssh access to these servers
46enabled = false
47
48[postfix]
49enabled = true
50mode = aggressive
51findtime = 7d # watch out for bad long-term trickle tricksters
52
53[postfix-rspamd]
54enabled = true
55findtime = 7d
56maxretry = 3
57
58[dovecot]
59enabled = true
diff --git a/ansible/roles/fail2ban/handlers/main.yml b/ansible/roles/fail2ban/handlers/main.yml
new file mode 100644
index 0000000..d83f78d
--- /dev/null
+++ b/ansible/roles/fail2ban/handlers/main.yml
@@ -0,0 +1,5 @@
1---
2- name: restart fail2ban
3 service:
4 name: fail2ban
5 state: restarted
diff --git a/ansible/roles/fail2ban/tasks/main.yml b/ansible/roles/fail2ban/tasks/main.yml
new file mode 100644
index 0000000..6d8e237
--- /dev/null
+++ b/ansible/roles/fail2ban/tasks/main.yml
@@ -0,0 +1,28 @@
1---
2# dovecot is configured to respect the fail2ban deny decisions
3# A failed login is recorded as:
4# dovecot[<pid>]: imap-login: access(tcpwrap): Client refused (rip=<ip>)
5- name: install fail2ban
6 apt:
7 name: fail2ban
8 state: latest
9 install_recommends: false
10
11- name: copy fail2ban config
12 copy:
13 src: fail2ban/
14 dest: /etc/fail2ban/
15 mode: preserve
16 notify:
17 - restart fail2ban
18
19
20# verify everything is running
21- name: verify services are running in dependency order
22 service:
23 name: "{{ item }}"
24 enabled: yes
25 state: started
26 loop:
27 - fail2ban
28
diff --git a/ansible/roles/gpg/tasks/main.yml b/ansible/roles/gpg/tasks/main.yml
new file mode 100644
index 0000000..6bb9299
--- /dev/null
+++ b/ansible/roles/gpg/tasks/main.yml
@@ -0,0 +1,6 @@
1---
2# gpg is required to verify keys of external apt repos
3- name: install gpg
4 apt:
5 pkg: gpg
6 state: latest
diff --git a/ansible/roles/network/tasks/main.yml b/ansible/roles/network/tasks/main.yml
new file mode 100644
index 0000000..5aaaeff
--- /dev/null
+++ b/ansible/roles/network/tasks/main.yml
@@ -0,0 +1,40 @@
1---
2#- name: plop netplan
3# copy:
4# src: "{{ inventory_hostname }}.yaml"
5# dest: "/etc/netplan/20-customNetworking.yaml"
6# register: netplanCopy
7#
8#- name: generate netplan
9# command: netplan generate
10# register: netplanGenerated
11# when: netplanCopy.changed
12#
13#- name: apply netplan
14# command: netplan apply
15# when: netplanGenerated.changed
16
17- name: configure public network link through systemd directly
18 template:
19 src: network/custom.link
20 dest: "/etc/systemd/network/custom-{{ network.ethernets.interface }}.link"
21 register: networkUpdated
22
23- name: configure public network address through systemd directly
24 template:
25 src: network/custom.network
26 dest: "/etc/systemd/network/custom-{{ network.ethernets.interface }}.network"
27 register: networkUpdated
28
29- name: reload network since configuration changed
30 command: systemctl restart systemd-networkd
31 when: networkUpdated.changed
32
33- name: re-query ansible facts since system changed
34 setup:
35 gather_subset:
36 - "!all"
37 - "!min"
38 - network
39 when: networkUpdated.changed
40
diff --git a/ansible/roles/network/templates/network/custom.link b/ansible/roles/network/templates/network/custom.link
new file mode 100644
index 0000000..e3bc5a8
--- /dev/null
+++ b/ansible/roles/network/templates/network/custom.link
@@ -0,0 +1,6 @@
1[Match]
2OriginalName={{ network.ethernets.interface }}
3
4[Link]
5WakeOnLan=off
6MACAddress={{ network.ethernets.macaddress }}
diff --git a/ansible/roles/network/templates/network/custom.network b/ansible/roles/network/templates/network/custom.network
new file mode 100644
index 0000000..5a8b74e
--- /dev/null
+++ b/ansible/roles/network/templates/network/custom.network
@@ -0,0 +1,38 @@
1[Match]
2Name={{ network.ethernets.interface }}
3
4#{% for net in network.ethernets.networks %}
5#[Network]
6#Address={{ net.subnet }}
7#{% if net.gateway is defined %}
8#Gateway={{ net.gateway }}
9#{% endif %}
10#
11#{% endfor %}
12
13
14{% for addr in network.ethernets.addresses %}
15[Address]
16Address={{ addr }}
17
18{% endfor %}
19
20{% for net in network.ethernets.networks %}
21[Network]
22Address={{ net.subnet }}
23{% if net.gateway is defined %}
24Gateway={{ net.gateway }}
25{% endif %}
26
27{% endfor %}
28
29
30{% for route in network.ethernets.routes %}
31[Route]
32Destination={{ route.to }}
33Gateway={{ route.via }}
34{% if route['on-link'] %}
35GatewayOnlink=true
36{% endif %}
37
38{% endfor %}
diff --git a/ansible/roles/nginx/defaults/main.yml b/ansible/roles/nginx/defaults/main.yml
new file mode 100644
index 0000000..34ac696
--- /dev/null
+++ b/ansible/roles/nginx/defaults/main.yml
@@ -0,0 +1,6 @@
1---
2nginx:
3 # Don't disable anything by default, but provide it here
4 # so you don't need to include empty 'disabled' in your own
5 # host configs.
6 disabled: []
diff --git a/ansible/roles/nginx/files/conf.d/http.custom.conf b/ansible/roles/nginx/files/conf.d/http.custom.conf
new file mode 100644
index 0000000..5f70166
--- /dev/null
+++ b/ansible/roles/nginx/files/conf.d/http.custom.conf
@@ -0,0 +1,84 @@
1
2## Proxy options
3proxy_buffering on;
4# proxy_cache_min_uses 3;
5proxy_cache_path /var/nginx/proxy-cache/ levels=1:2 keys_zone=cache:10m inactive=10m max_size=1000M;
6proxy_cache_valid any 10m;
7proxy_ignore_client_abort off;
8proxy_intercept_errors on;
9proxy_next_upstream error timeout invalid_header;
10proxy_redirect off;
11proxy_set_header Host $host;
12proxy_set_header X-Forwarded-For $remote_addr;
13proxy_connect_timeout 60;
14proxy_send_timeout 60;
15proxy_read_timeout 60;
16
17# We used to use this header when we ran dual http/https stacks to verify
18# user login pages were being only requested over https, but now we forward
19# every site to https, so we can assume our schemes are aligned to our interests
20# (as long as all our backend code stopped checking for X-Forwarded-Proto too).
21#proxy_set_header X-Forwarded-Proto $scheme;
22
23## Size Limits
24# May need to override these (server or location blocks) if doing large uploads.
25# Setting to zero disables any size checking.
26client_body_buffer_size 16k;
27client_max_body_size 15m;
28
29# If clients send headers larger than 1k,
30# they get upgraded to large_client_header_buffers.
31client_header_buffer_size 1k;
32large_client_header_buffers 32 64k;
33
34## Timeouts
35client_body_timeout 5s;
36client_header_timeout 5s;
37keepalive_timeout 5s 5s;
38#keepalive_timeout 0;
39send_timeout 5s;
40
41## General Options
42ignore_invalid_headers on;
43recursive_error_pages on;
44#sendfile on; # enabled by top level config
45server_name_in_redirect off;
46server_tokens off;
47
48# For per-client rate limiting, see config options at:
49# https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html
50
51## Compression
52#gzip on; # enabled by top level config
53gzip_static on;
54gzip_buffers 16 32k;
55gzip_comp_level 6;
56gzip_http_version 1.0;
57gzip_min_length 500;
58gzip_types text/plain application/x-javascript text/xml text/css image/x-icon application/xml application/xml+rss text/javascript application/javascript application/json image/svg+xml font/truetype font/opentype application/vnd.ms-fontobject;
59gzip_vary on;
60gzip_proxied any; # required for cloudfront to receive a gzip'd response
61
62## Filesystem Operation Cache (caches fds, sizes, times, errors, etc)
63open_file_cache max=6000 inactive=5m;
64open_file_cache_valid 2m;
65open_file_cache_min_uses 1;
66open_file_cache_errors on;
67
68# For reading a response from disk
69output_buffers 32 32k;
70
71## Optimize Large File Transfers (can be overriden in hosts and locations)
72aio threads; # use default thread pool, create thread pools: threads=NAME;
73aio_write on; # use threaded writes for temporary files and proxied data
74
75# For files larger than 8 MB, use O_DIRECT instead of sendfile()
76directio 8m;
77directio_alignment 512; # if using XFS, set as 4096
78
79## Access Log Caches
80open_log_file_cache max=64 inactive=20s min_uses=1 valid=1m;
81
82log_format main '$remote_addr - $remote_user [$time_local] "$request" '
83 '$status $body_bytes_sent "$http_referer" '
84 '"$http_user_agent" "$http_x_forwarded_for"';
diff --git a/ansible/roles/nginx/files/tls/ssl_ciphers_intermediate b/ansible/roles/nginx/files/tls/ssl_ciphers_intermediate
new file mode 100644
index 0000000..bc79954
--- /dev/null
+++ b/ansible/roles/nginx/files/tls/ssl_ciphers_intermediate
@@ -0,0 +1,10 @@
1# From https://mozilla.github.io/server-side-tls/ssl-config-generator/
2# as of 2018-07-12
3
4# No TLSv1.3 support yet!
5
6ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
7ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
8
9# Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
10ssl_dhparam /etc/ssl/ffdhe2048.pem;
diff --git a/ansible/roles/nginx/files/tls/ssl_ciphers_modern b/ansible/roles/nginx/files/tls/ssl_ciphers_modern
new file mode 100644
index 0000000..ab93ffc
--- /dev/null
+++ b/ansible/roles/nginx/files/tls/ssl_ciphers_modern
@@ -0,0 +1,7 @@
1# From https://mozilla.github.io/server-side-tls/ssl-config-generator/
2# as of 2018-07-12
3
4# No TLSv1.3 support yet!
5
6ssl_protocols TLSv1.2;
7ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
diff --git a/ansible/roles/nginx/files/tls/ssl_ciphers_tls13 b/ansible/roles/nginx/files/tls/ssl_ciphers_tls13
new file mode 100644
index 0000000..db04c36
--- /dev/null
+++ b/ansible/roles/nginx/files/tls/ssl_ciphers_tls13
@@ -0,0 +1,7 @@
1# From https://github.com/cloudflare/sslconfig/blob/796bc5ac7224f1e540394d792323ccafa86aaeea/conf
2
3# nginx >= 1.11.0 (2016-05-24) created the 'ssl_ecdh_curve' parameter
4
5ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
6ssl_ecdh_curve X25519:P-256:P-384:P-224:P-521;
7ssl_ciphers '[ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305|ECDHE-RSA-AES128-GCM-SHA256|ECDHE-RSA-CHACHA20-POLY1305]:ECDHE+AES128:RSA+AES128:ECDHE+AES256:RSA+AES256:ECDHE+3DES:RSA+3DES';
diff --git a/ansible/roles/nginx/files/tls/ssl_params b/ansible/roles/nginx/files/tls/ssl_params
new file mode 100644
index 0000000..37798fc
--- /dev/null
+++ b/ansible/roles/nginx/files/tls/ssl_params
@@ -0,0 +1,55 @@
1# Test OCSP with:
2# openssl s_client -connect $site:443 -tls1 -tlsextdebug -status
3#
4# also test with:
5# openssl s_client -connect $site:443 -CAfile /etc/ssl/certs/ca-certificates.crt -showcerts -status -tlsextdebug -cipher RSA </dev/null
6#
7# openssl s_client -connect $site:443 -CAfile /etc/ssl/certs/ca-certificates.crt -showcerts -status -tlsextdebug -cipher ECDSA </dev/null
8
9
10# Duration client SSL session tickets are valid for:
11ssl_session_timeout 1d;
12# NOTE NOTE NOTE NOTE NOTE
13# nginx only regenerates its ssl_session_ticket_key on reload or restart.
14# the ticket key is basically a symmetric key that effectively breaks
15# forward secrecy if leaked.
16# With ssl_session_tickets enabled, you should reload nginx daily to reset
17# the internal cached ticket key.
18# If you are using external ticket keys, those should also be rotated daily.
19# END NOTE END NOTE END NOTE
20
21# Internal cache of SSL sessions
22ssl_session_cache shared:SSL:500m; # 500MB = 2M cached sessions (4k sessions/MB)
23
24# session tickets are reused for the life of the server.
25# For multiple servers serving the same host,
26# have them all share the same key and rotate as necessary:
27# ssl_session_ticket_key [keyfile];
28# Without a ticket key file defined, a reload of nginx resets the key.
29ssl_session_tickets on;
30
31# Individual cipher files are included externally
32# (one of ssl_ciphers_{intermediate,modern})
33ssl_prefer_server_ciphers on;
34
35# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
36add_header Strict-Transport-Security "max-age=15768000; includeSubdomains";
37
38# OCSP Stapling ---
39# fetch OCSP records from URL in ssl_certificate and cache them
40ssl_stapling on;
41ssl_stapling_verify on;
42
43# See: https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_trusted_certificate
44ssl_trusted_certificate /etc/ssl/lets-encrypt-x3-cross-signed.pem;
45
46# Instead of using resolver, take response from file:
47# ssl_stapling_file <-- must be set PER domain, but nginx so far has refused
48# to add the ability to have one stapling file per certificate now that nginx
49# supports both RSA and EC per domain. So, this is useless if you have multiple
50# certificates per domain.
51
52# 'valid' ignores DNS TTL and caches lookups for specified duration
53# This should be replaced with a local dnsmasq resolver at 127.0.0.1
54resolver 127.0.0.53 4.2.2.2 8.8.8.8 1.1.1.1 valid=600s ipv6=off;
55resolver_timeout 4s;
diff --git a/ansible/roles/nginx/handlers/main.yml b/ansible/roles/nginx/handlers/main.yml
new file mode 100644
index 0000000..31f8ade
--- /dev/null
+++ b/ansible/roles/nginx/handlers/main.yml
@@ -0,0 +1,3 @@
1---
2- name: reload nginx
3 command: nginx -s reload
diff --git a/ansible/roles/nginx/tasks/main.yml b/ansible/roles/nginx/tasks/main.yml
new file mode 100644
index 0000000..73469a1
--- /dev/null
+++ b/ansible/roles/nginx/tasks/main.yml
@@ -0,0 +1,118 @@
1---
2- name: emerge, nginx with extra modules!
3 apt:
4 pkg: nginx-extras
5 state: latest
6
7# Keep 32 logs
8- name: adjust nginx logrotate keep files
9 lineinfile:
10 state: present
11 path: /etc/logrotate.d/nginx
12 regexp: "^(\\s+)rotate "
13 line: "\\1rotate 32"
14 backrefs: yes
15
16# And only rotate when they grow larger than 1 GB
17- name: adjust nginx logrotate trigger rolls
18 lineinfile:
19 state: present
20 path: /etc/logrotate.d/nginx
21 regexp: "minsize"
22 line: "minsize 1G"
23 insertafter: "rotate \\d+"
24
25- name: verify nginx isn't serving default pages
26 file:
27 path: /etc/nginx/sites-enabled/default
28 state: absent
29 notify:
30 - reload nginx
31
32- name: verify nginx proxy cache dir exists
33 file:
34 path: /var/nginx/proxy-cache
35 owner: www-data
36 state: directory
37
38- name: verify nginx cpu affinity
39 lineinfile:
40 state: present
41 path: /etc/nginx/nginx.conf
42 regexp: "^worker_cpu_affinity "
43 line: "worker_cpu_affinity auto;"
44 insertafter: '^worker_processes '
45 notify:
46 - reload nginx
47
48- name: drop keepalive from nginx conf because we set it custom
49 lineinfile:
50 state: absent
51 path: /etc/nginx/nginx.conf
52 regexp: "^\\s+keepalive_timeout"
53 notify:
54 - reload nginx
55
56- name: copy config extensions
57 copy:
58 src: conf.d
59 dest: /etc/nginx/
60 notify:
61 - reload nginx
62
63- name: copy shared tls settings
64 copy:
65 src: tls/
66 dest: /etc/nginx/
67 notify:
68 - reload nginx
69
70- name: generate our templated basic sites
71 template:
72 src: basic-site.conf.j2
73 dest: "/etc/nginx/sites-available/{{ item.domain }}"
74 loop: "{{ nginx.basic }}"
75 notify:
76 - reload nginx
77
78- name: copy our more complex sites we don't want templated
79 copy:
80 src: "servers/{{ item }}"
81 dest: /etc/nginx/sites-available/
82 loop: "{{ nginx.complex }}"
83 notify:
84 - reload nginx
85
86- name: activate our nginx site configs
87 file:
88 src: "/etc/nginx/sites-available/{{ item }}"
89 dest: "/etc/nginx/sites-enabled/{{ item }}"
90 state: link
91 loop: "{{ nginx.complex }}"
92 notify:
93 - reload nginx
94
95- name: activate our nginx site templates
96 file:
97 src: "/etc/nginx/sites-available/{{ item.domain }}"
98 dest: "/etc/nginx/sites-enabled/{{ item.domain }}"
99 state: link
100 loop: "{{ nginx.basic }}"
101 notify:
102 - reload nginx
103
104- name: remove disabled sites
105 file:
106 src: "/etc/nginx/sites-enabled/{{ item }}"
107 state: absent
108 loop: "{{ nginx.disabled | default([]) }}"
109 notify:
110 - reload nginx
111
112- name: reload if certs newish
113 include_role:
114 name: certreload
115 vars:
116 certreload:
117 notifiers:
118 - reload nginx
diff --git a/ansible/roles/nginx/templates/basic-site.conf.j2 b/ansible/roles/nginx/templates/basic-site.conf.j2
new file mode 100644
index 0000000..454b2bd
--- /dev/null
+++ b/ansible/roles/nginx/templates/basic-site.conf.j2
@@ -0,0 +1,68 @@
1server {
2 listen {{ item.domain }}:443 ssl http2 fastopen=4096 reuseport;
3 server_name {{ item.domain }};
4
5 access_log /var/log/nginx/{{ item.domain }}.access.log main buffer=32k;
6 error_log /var/log/nginx/{{ item.domain }}.error.log error;
7
8 ssl on;
9
10 include /etc/nginx/ssl_params;
11
12{% if nginx.ssl == "modern" %}
13 include /etc/nginx/ssl_ciphers_modern;
14{% elif nginx.ssl == "tls13" %}
15 include /etc/nginx/ssl_ciphers_tls13;
16{% else %}
17 # Default, just use commonly accepted options:
18 include /etc/nginx/ssl_ciphers_intermediate;
19{% endif %}
20
21 ssl_certificate /etc/ssl/{{ item.domain }}-cert-combined.rsa2048.pem;
22 ssl_certificate_key /etc/ssl/private/{{ item.domain }}-key.rsa2048.pem;
23
24 # nginx >= 1.11.0 (2016-05-24) allows loading redundant certs and keys so you
25 # can serve modern EC clients and less modern RSA clients at the same time.
26 ssl_certificate /etc/ssl/{{ item.domain }}-cert-combined.prime256v1.pem;
27 ssl_certificate_key /etc/ssl/private/{{ item.domain }}-key.prime256v1.pem;
28
29 root /srv/web/{{ item.domain }};
30
31{% if nginx.google is defined %}
32 location /{{ nginx.google.siteKey }}.html {
33 root {{ nginx.google.siteKeyServeDir }}};
34 }
35{% endif %}
36
37{% if item.customConfig is defined %}
38{{ item.customConfig }}
39{% endif %}
40
41{% for location in item.uri %}
42 location {{ location.path }} {
43{% if location.appServer is defined %}
44 proxy_pass {{ location.appServer }}/$request_uri;
45 proxy_set_header Host $host;
46{% else %}
47 root /srv/web/{{ item.domain }};
48{% endif %}
49 }
50{% endfor %}
51}
52
53server {
54 listen {{ item.domain }} fastopen=4096 reuseport;
55 server_name www.{{ item.domain }} {{ item.domain }};
56
57 access_log /var/log/nginx/{{ item.domain }}.access.log main buffer=32k;
58 error_log /var/log/nginx/{{ item.domain }}.error.log error;
59
60 location /.well-known/acme-challenge/ {
61 alias /srv/web/challenges/;
62 try_files $uri =404;
63 }
64
65 location / {
66 return 301 https://{{ item.domain }}$request_uri;
67 }
68}
diff --git a/ansible/roles/pip3/tasks/main.yml b/ansible/roles/pip3/tasks/main.yml
new file mode 100644
index 0000000..26de65b
--- /dev/null
+++ b/ansible/roles/pip3/tasks/main.yml
@@ -0,0 +1,6 @@
1---
2# pip3 because borgmatic is distributed through pip3, not apt/dpkg
3- name: install pip3
4 apt:
5 pkg: python3-pip
6 state: latest
diff --git a/ansible/roles/postfix/files/postfix/master.cf b/ansible/roles/postfix/files/postfix/master.cf
new file mode 100644
index 0000000..d428c18
--- /dev/null
+++ b/ansible/roles/postfix/files/postfix/master.cf
@@ -0,0 +1,113 @@
1#
2# Postfix master process configuration file. For details on the format
3# of the file, see the master(5) manual page (command: "man 5 master" or
4# on-line: http://www.postfix.org/master.5.html).
5#
6# Do not forget to execute "postfix reload" after editing this file.
7#
8# ==========================================================================
9# service type private unpriv chroot wakeup maxproc command + args
10# (yes) (yes) (no) (never) (100)
11# ==========================================================================
12smtp inet n - y - - smtpd
13 -o smtpd_sasl_auth_enable=no
14# for verbose connection debugging, append -v to the above args
15#smtp inet n - y - 1 postscreen
16#smtpd pass - - y - - smtpd
17#dnsblog unix - - y - 0 dnsblog
18#tlsproxy unix - - y - 0 tlsproxy
19submission inet n - n - - smtpd
20 -o smtpd_tls_security_level=encrypt
21 -o tls_preempt_cipherlist=yes
22#submission inet n - y - - smtpd
23# -o syslog_name=postfix/submission
24# -o smtpd_tls_security_level=encrypt
25# -o smtpd_sasl_auth_enable=yes
26# -o smtpd_tls_auth_only=yes
27# -o smtpd_reject_unlisted_recipient=no
28# -o smtpd_client_restrictions=$mua_client_restrictions
29# -o smtpd_helo_restrictions=$mua_helo_restrictions
30# -o smtpd_sender_restrictions=$mua_sender_restrictions
31# -o smtpd_recipient_restrictions=
32# -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
33# -o milter_macro_daemon_name=ORIGINATING
34#smtps inet n - y - - smtpd
35# -o syslog_name=postfix/smtps
36# -o smtpd_tls_wrappermode=yes
37# -o smtpd_sasl_auth_enable=yes
38# -o smtpd_reject_unlisted_recipient=no
39# -o smtpd_client_restrictions=$mua_client_restrictions
40# -o smtpd_helo_restrictions=$mua_helo_restrictions
41# -o smtpd_sender_restrictions=$mua_sender_restrictions
42# -o smtpd_recipient_restrictions=
43# -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
44# -o milter_macro_daemon_name=ORIGINATING
45#628 inet n - y - - qmqpd
46pickup unix n - y 60 1 pickup
47cleanup unix n - y - 0 cleanup
48qmgr unix n - n 300 1 qmgr
49#qmgr unix n - n 300 1 oqmgr
50tlsmgr unix - - y 1000? 1 tlsmgr
51rewrite unix - - y - - trivial-rewrite
52bounce unix - - y - 0 bounce
53defer unix - - y - 0 bounce
54trace unix - - y - 0 bounce
55verify unix - - y - 1 verify
56flush unix n - y 1000? 0 flush
57proxymap unix - - n - - proxymap
58proxywrite unix - - n - 1 proxymap
59smtp unix - - y - - smtp
60relay unix - - y - - smtp
61 -o syslog_name=postfix/$service_name
62# -o smtp_helo_timeout=5 -o smtp_connect_timeout=5
63showq unix n - y - - showq
64error unix - - y - - error
65retry unix - - y - - error
66discard unix - - y - - discard
67local unix - n n - - local
68virtual unix - n n - - virtual
69lmtp unix - - y - - lmtp
70anvil unix - - y - 1 anvil
71scache unix - - y - 1 scache
72#
73# ====================================================================
74# Interfaces to non-Postfix software. Be sure to examine the manual
75# pages of the non-Postfix software to find out what options it wants.
76#
77# Many of the following services use the Postfix pipe(8) delivery
78# agent. See the pipe(8) man page for information about ${recipient}
79# and other message envelope options.
80# ====================================================================
81#
82# maildrop. See the Postfix MAILDROP_README file for details.
83# Also specify in main.cf: maildrop_destination_recipient_limit=1
84#
85maildrop unix - n n - - pipe
86 flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient}
87#
88# ====================================================================
89#
90# Recent Cyrus versions can use the existing "lmtp" master.cf entry.
91#
92# Specify in cyrus.conf:
93# lmtp cmd="lmtpd -a" listen="localhost:lmtp" proto=tcp4
94#
95# Specify in main.cf one or more of the following:
96# mailbox_transport = lmtp:inet:localhost
97# virtual_transport = lmtp:inet:localhost
98#
99# ====================================================================
100#
101# Cyrus 2.1.5 (Amos Gouaux)
102# Also specify in main.cf: cyrus_destination_recipient_limit=1
103#
104#cyrus unix - n n - - pipe
105# user=cyrus argv=/cyrus/bin/deliver -e -r ${sender} -m ${extension} ${user}
106#
107# ====================================================================
108# Old example of delivery via Cyrus.
109#
110#old-cyrus unix - n n - - pipe
111# flags=R user=cyrus argv=/cyrus/bin/deliver -e -m ${extension} ${user}
112#
113# ====================================================================
diff --git a/ansible/roles/postfix/files/postfix/remap.sh b/ansible/roles/postfix/files/postfix/remap.sh
new file mode 100755
index 0000000..86e8d73
--- /dev/null
+++ b/ansible/roles/postfix/files/postfix/remap.sh
@@ -0,0 +1,9 @@
1#!/usr/bin/env bash
2
3here=$(dirname $0)
4
5MAPS="virtual"
6
7for map in $MAPS; do
8 postmap $here/$map
9done
diff --git a/ansible/roles/postfix/handlers/main.yml b/ansible/roles/postfix/handlers/main.yml
new file mode 100644
index 0000000..aa41be4
--- /dev/null
+++ b/ansible/roles/postfix/handlers/main.yml
@@ -0,0 +1,13 @@
1---
2- name: restart postfix
3 service:
4 name: postfix
5 state: restarted
6
7- name: reload postfix
8 service:
9 name: postfix
10 state: reloaded
11
12- name: rehash postfix aliases
13 command: /etc/postfix/remap.sh
diff --git a/ansible/roles/postfix/tasks/main.yml b/ansible/roles/postfix/tasks/main.yml
new file mode 100644
index 0000000..57b0601
--- /dev/null
+++ b/ansible/roles/postfix/tasks/main.yml
@@ -0,0 +1,50 @@
1---
2# postfix install and configuration
3# note: this postfix config requires open ports: 25 and 587
4- name: install postfix
5 apt:
6 state: latest
7 pkg:
8 - postfix
9 - postfix-doc
10 - postfix-pcre
11
12- name: give postfix user permission to read private keys
13 user:
14 name: postfix
15 groups: ssl-cert
16 append: yes
17
18- name: copy postfix config
19 copy:
20 src: postfix/
21 dest: /etc/postfix/
22 mode: preserve
23 notify:
24 - restart postfix # NB this could be a reload instead
25 - rehash postfix aliases
26
27- name: instantiate postfix main.cf template
28 template:
29 src: postfix/main.cf.j2
30 dest: /etc/postfix/main.cf
31 notify:
32 - reload postfix
33
34
35# verify everything is running
36- name: verify services are running in dependency order
37 service:
38 name: "{{ item }}"
39 enabled: yes
40 state: started
41 loop:
42 - postfix
43
44- name: reload if certs newish
45 include_role:
46 name: certreload
47 vars:
48 certreload:
49 notifiers:
50 - reload postfix
diff --git a/ansible/roles/postfix/templates/postfix/main.cf.j2 b/ansible/roles/postfix/templates/postfix/main.cf.j2
new file mode 100644
index 0000000..b3e8cc2
--- /dev/null
+++ b/ansible/roles/postfix/templates/postfix/main.cf.j2
@@ -0,0 +1,194 @@
1# Modified from https://www.c0ffee.net/blog/mail-server-guide
2
3smtpd_banner = $myhostname ESMTP dx Independence
4
5# "2" is current for postfix 3.2 configs
6compatibility_level = 2
7
8# disable "new mail" notifications for local unix users
9biff = no
10
11# directory to store mail for local unix users
12mail_spool_directory = /var/mail/local
13
14# Name of this mail server, used in the SMTP HELO for outgoing mail. Make
15# sure this resolves to the same IP as your reverse DNS hostname.
16myhostname = {{ network.hostname.public }}
17
18# Domains for which postfix will deliver local mail. Does not apply to
19# virtual domains, which are configured below. Make sure to specify the FQDN
20# of your sever, as well as localhost.
21# Note: NEVER specify any virtual domains here!!! Those come later.
22mydestination = localhost
23
24# Domain appended to mail sent locally from this machine - such as mail sent
25# via the `sendmail` command.
26myorigin = $myhostname
27
28# prevent spammers from searching for valid users
29disable_vrfy_command = yes
30
31# require properly formatted email addresses - prevents a lot of spam
32strict_rfc821_envelopes = yes
33
34# don't give any helpful info when a mailbox doesn't exist
35show_user_unknown_table_name = no
36
37# limit maximum e-mail size to 256 MB. mailbox size must be at least as big as
38# the message size for the mail to be accepted, but has no meaning after
39# that since we are using Dovecot for delivery.
40message_size_limit = 268435456
41mailbox_size_limit = 0
42
43# require addresses of the form "[email protected]"
44allow_percent_hack = no
45swap_bangpath = no
46
47# allow plus-aliasing: "[email protected]" delivers to "user" mailbox
48recipient_delimiter = +
49
50# path to the SSL certificate for the mail server
51smtpd_tls_cert_file = /etc/ssl/{{ network.hostname.public }}-cert-combined.rsa2048.pem
52smtpd_tls_key_file = /etc/ssl/private/{{ network.hostname.public }}-key.rsa2048.pem
53
54# You can also specify an EC cert to try first if the clients support it.
55smtpd_tls_eccert_file = /etc/ssl/{{ network.hostname.public }}-cert-combined.prime256v1.pem
56smtpd_tls_eckey_file = /etc/ssl/private/{{ network.hostname.public }}-key.prime256v1.pem
57
58# Path to your trusted certificates file. Usually provided by a
59# ca-certificates package or similar.
60smtp_tls_CAfile=/etc/ssl/certs/ca-certificates.crt
61
62# These two lines define how postfix will connect to other mail servers.
63# "may" allows opportunistic TLS and "enabled" allows hostname lookups
64# http://www.postfix.org/TLS_README.html
65smtp_tls_security_level = may
66smtp_dns_support_level = enabled
67
68# IP address used by postfix to send outgoing mail. You only need this if
69# your machine has multiple IP addresses - set it to your MX address to
70# satisfy your SPF record.
71smtp_bind_address = {{ hostvars[inventory_hostname]['ansible_' + network.interface.public]['ipv4']['address'] }}
72smtp_bind_address6 =
73inet_interfaces = 127.0.0.1,$smtp_bind_address
74inet_protocols = ipv4
75
76# Here we define the options for "mandatory" TLS. In our setup, TLS is only
77# "mandatory" for authenticating users. I got these settings from Mozilla's
78# SSL reccomentations page.
79#
80# NOTE: do not attempt to make TLS mandatory for all incoming/outgoing
81# connections. Do not attempt to change the default cipherlist for non-
82# mandatory connections either. There are still a lot of mail servers out
83# there that do not use TLS, and many that do only support old ciphers.
84# Forcing TLS for everyone *will* cause you to lose mail.
85smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, TLSv1.2
86smtpd_tls_mandatory_ciphers = high
87tls_high_cipherlist = ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
88
89# allow other mail servers to connect using TLS, but don't require it
90smtpd_tls_security_level = may
91
92# tickets and compression have known vulnerabilities
93tls_ssl_options = no_ticket, no_compression
94
95# yes, using 2048 with "dh1024" is the right thing to do
96smtpd_tls_dh1024_param_file = /etc/ssl/ffdhe2048.pem
97
98# cache incoming and outgoing TLS sessions
99smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_tlscache
100smtp_tls_session_cache_database = btree:${data_directory}/smtp_tlscache
101
102# enable SMTPD auth. Dovecot will place an `auth` socket in postfix's
103# runtime directory that we will use for authentication.
104# TODO: can also replace this with a network inet connection if dovecot opens it
105smtpd_sasl_auth_enable = yes
106smtpd_sasl_path = private/auth
107smtpd_sasl_type = dovecot
108
109# only allow authentication over TLS
110smtpd_tls_auth_only = yes
111
112# don't allow plaintext auth methods on unencrypted connections
113smtpd_sasl_security_options = noanonymous, noplaintext
114# but plaintext auth is fine when using TLS
115smtpd_sasl_tls_security_options = noanonymous
116
117# add a message header when email was recieved over TLS
118smtpd_tls_received_header = yes
119
120# require that connecting mail servers identify themselves - this greatly
121# reduces spam
122smtpd_helo_required = yes
123
124# The following block specifies some security restrictions for incoming
125# mail. The gist of it is, authenticated users and connections from
126# localhost can do anything they want. Random people connecting over the
127# internet are treated with more suspicion: they must have a reverse DNS
128# entry and present a valid, FQDN HELO hostname. In addition, they can only
129# send mail to valid mailboxes on the server, and the sender's domain must
130# actually exist.
131smtpd_client_restrictions =
132 permit_mynetworks,
133 permit_sasl_authenticated,
134 #reject_unknown_reverse_client_hostname,
135 # you might want to consider:
136 # reject_unknown_client_hostname,
137 # here. This will reject all incoming connections without a reverse DNS
138 # entry that resolves back to the client's IP address. This is a very
139 # restrictive check and may reject legitimate mail.
140 reject_unauth_pipelining
141smtpd_helo_restrictions =
142 permit_mynetworks,
143 permit_sasl_authenticated,
144 reject_invalid_helo_hostname,
145 reject_non_fqdn_helo_hostname,
146 # you might want to consider:
147 # reject_unknown_helo_hostname,
148 # here. This will reject all incoming mail without a HELO hostname that
149 # properly resolves in DNS. This is a somewhat restrictive check and may
150 # reject legitimate mail.
151 reject_unauth_pipelining
152smtpd_sender_restrictions =
153 permit_mynetworks,
154 permit_sasl_authenticated,
155 reject_non_fqdn_sender,
156# reject_unknown_sender_domain,
157 reject_unauth_pipelining
158smtpd_relay_restrictions =
159 permit_mynetworks,
160 permit_sasl_authenticated,
161 # !!! THIS SETTING PREVENTS YOU FROM BEING AN OPEN RELAY !!!
162 reject_unauth_destination
163 # !!! DO NOT REMOVE IT UNDER ANY CIRCUMSTANCES !!!
164#smtpd_recipient_restrictions =
165# permit_mynetworks,
166# permit_sasl_authenticated,
167# reject_non_fqdn_recipient,
168# reject_unknown_recipient_domain,
169# reject_unauth_pipelining,
170smtpd_data_restrictions =
171 permit_mynetworks,
172 permit_sasl_authenticated,
173 reject_multi_recipient_bounce,
174 reject_unauth_pipelining
175
176smtpd_recipient_restrictions =
177 permit_mynetworks,
178 permit_sasl_authenticated,
179 reject_unauth_destination
180
181# deliver mail for virtual users to Dovecot's LMTP socket
182# TODO: convert this to network with dovecot opening a local inet port
183virtual_transport = lmtp:unix:private/dovecot-lmtp
184virtual_mailbox_domains = /etc/postfix/domains
185
186virtual_alias_maps = hash:/etc/postfix/virtual
187#virtual_alias_domains = /etc/postfix/domains
188
189# We'll uncomment these when we set up rspamd later:
190milter_protocol = 6
191milter_default_action = accept
192#smtpd_milters = unix:/var/run/rspamd/milter.sock
193smtpd_milters = inet:localhost:11332
194milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
diff --git a/ansible/roles/ramdisk/tasks/main.yml b/ansible/roles/ramdisk/tasks/main.yml
new file mode 100644
index 0000000..2c05747
--- /dev/null
+++ b/ansible/roles/ramdisk/tasks/main.yml
@@ -0,0 +1,13 @@
1- name: create ramdisk directory
2 file:
3 dest: /srv/ramdisk
4 mode: 0755 # TODO: use acl module and bind this to system data user
5 state: directory
6
7- name: mount ramdisk to /srv/ramdisk
8 mount:
9 path: /srv/ramdisk
10 src: tmpfs
11 fstype: tmpfs
12 opts: "lazytime,size={{ ansible_memtotal_mb }}M"
13 state: mounted
diff --git a/ansible/roles/rspamd/files/redis/redis.conf b/ansible/roles/rspamd/files/redis/redis.conf
new file mode 100644
index 0000000..ee237d9
--- /dev/null
+++ b/ansible/roles/rspamd/files/redis/redis.conf
@@ -0,0 +1,21 @@
1# listen only on localhost
2# Even though we only tell rspamd to contact Redis over 127.0.0.1, it still
3# connects via ::1 for some modules as well, so things inside rspamd break
4# if redis isn't listening on both 127.0.0.1 and ::1
5bind 127.0.0.1 ::1
6
7# limit the max amount of memory used - appropriate value will depend on
8# your email volume
9maxmemory 512mb
10maxmemory-policy volatile-lru
11
12daemonize yes
13
14logfile /var/log/redis/redis-server.log
15dir /var/lib/redis
16
17appendonly yes
18appendfilename redisisgarbage.aof
19dbfilename redisisgarbage.rdb
20
21pidfile /var/run/redis/redis-server.pid
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/classifier-bayes.conf b/ansible/roles/rspamd/files/rspamd/local.d/classifier-bayes.conf
new file mode 100644
index 0000000..db2f240
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/classifier-bayes.conf
@@ -0,0 +1,4 @@
1autolearn = true;
2backend = "redis";
3new_schema = true;
4expire = 8640000;
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/greylist.conf b/ansible/roles/rspamd/files/rspamd/local.d/greylist.conf
new file mode 100644
index 0000000..a6ee831
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/greylist.conf
@@ -0,0 +1 @@
enabled = false;
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/milter_headers.conf b/ansible/roles/rspamd/files/rspamd/local.d/milter_headers.conf
new file mode 100644
index 0000000..4c924d7
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/milter_headers.conf
@@ -0,0 +1,3 @@
1# Refer to https://rspamd.com/doc/modules/milter_headers.html for information on configuration
2
3use = ["x-spamd-bar", "authentication-results", "x-spamd-result", "x-rspamd-server", "x-rspamd-queue-id"];
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/mx_check.conf b/ansible/roles/rspamd/files/rspamd/local.d/mx_check.conf
new file mode 100644
index 0000000..de9fac1
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/mx_check.conf
@@ -0,0 +1,2 @@
1# checks if sender's domain has at least one connectable MX record
2enabled = true;
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/neural.conf b/ansible/roles/rspamd/files/rspamd/local.d/neural.conf
new file mode 100644
index 0000000..7fc7bd2
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/neural.conf
@@ -0,0 +1,33 @@
1servers = "localhost";
2enabled = true; # Important after 1.7
3
4# use_settings = true;
5
6rules {
7 "LONG" {
8 train {
9 max_trains = 5000;
10 max_usages = 200;
11 max_iterations = 25;
12 learning_rate = 0.01,
13 spam_score = 8;
14 ham_score = -2;
15 }
16 symbol_spam = "NEURAL_SPAM_LONG";
17 symbol_ham = "NEURAL_HAM_LONG";
18 ann_expire = 100d;
19 }
20 "SHORT" {
21 train {
22 max_trains = 100;
23 max_usages = 2;
24 max_iterations = 25;
25 learning_rate = 0.01,
26 spam_score = 8;
27 ham_score = -2;
28 }
29 symbol_spam = "NEURAL_SPAM_SHORT";
30 symbol_ham = "NEURAL_HAM_SHORT";
31 ann_expire = 1d;
32 }
33}
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/neural_group.conf b/ansible/roles/rspamd/files/rspamd/local.d/neural_group.conf
new file mode 100644
index 0000000..fff5058
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/neural_group.conf
@@ -0,0 +1,29 @@
1symbols = {
2 "NEURAL_SPAM" {
3 weight = 3.0; # sample weight
4 description = "Neural network spam";
5 }
6 "NEURAL_HAM" {
7 weight = -3.0; # sample weight
8 description = "Neural network ham";
9 }
10}
11
12symbols = {
13 "NEURAL_SPAM_LONG" {
14 weight = 3.0; # sample weight
15 description = "Neural network spam (long)";
16 }
17 "NEURAL_HAM_LONG" {
18 weight = -3.0; # sample weight
19 description = "Neural network ham (long)";
20 }
21 "NEURAL_SPAM_SHORT" {
22 weight = 2.0; # sample weight
23 description = "Neural network spam (short)";
24 }
25 "NEURAL_HAM_SHORT" {
26 weight = -1.0; # sample weight
27 description = "Neural network ham (short)";
28 }
29}
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/phishing.conf b/ansible/roles/rspamd/files/rspamd/local.d/phishing.conf
new file mode 100644
index 0000000..dcb1caf
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/phishing.conf
@@ -0,0 +1,3 @@
1# check messages against some anti-phishing databases
2openphish_enabled = true;
3phishtank_enabled = true;
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/redis.conf b/ansible/roles/rspamd/files/rspamd/local.d/redis.conf
new file mode 100644
index 0000000..4ae822d
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/redis.conf
@@ -0,0 +1,2 @@
1# just specifying a server enables redis for all modules that can use it
2servers = "127.0.0.1:6379";
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/replies.conf b/ansible/roles/rspamd/files/rspamd/local.d/replies.conf
new file mode 100644
index 0000000..382764d
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/replies.conf
@@ -0,0 +1,2 @@
1# whitelist messages from threads that have been replied to
2action = "no action";
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/surbl.conf b/ansible/roles/rspamd/files/rspamd/local.d/surbl.conf
new file mode 100644
index 0000000..9cbd70f
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/surbl.conf
@@ -0,0 +1,2 @@
1# follow redirects when checking URLs in emails for spaminess
2redirector_hosts_map = "/etc/rspamd/redirectors.inc";
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/url_reputation.conf b/ansible/roles/rspamd/files/rspamd/local.d/url_reputation.conf
new file mode 100644
index 0000000..f5ca48a
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/url_reputation.conf
@@ -0,0 +1,2 @@
1# check URLs within messages for spaminess
2enabled = true;
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/url_tags.conf b/ansible/roles/rspamd/files/rspamd/local.d/url_tags.conf
new file mode 100644
index 0000000..61832fb
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/url_tags.conf
@@ -0,0 +1,2 @@
1# cache some URL tags in redis
2enabled = true;
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/worker-controller.inc b/ansible/roles/rspamd/files/rspamd/local.d/worker-controller.inc
new file mode 100644
index 0000000..1e1a713
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/worker-controller.inc
@@ -0,0 +1,39 @@
1# generate a password hash using the `rspamadm pw` command and define 'password'
2# this one is the hash for 'hunter2'
3# password = "$2$b9s94udsn7zzgk1hc9wuheqqcpydo64x$a1kksr9r9f1g1358shqdz789wmoqbnapndwqi6uscazhz3muz4gy";
4
5# this one is the hash for:
6# Roberts had grown so rich, he wanted to retire. He took me to his cabin and he told me his secret. 'I am not the Dread Pirate Roberts' he said. 'My name is Ryan; I inherited the ship from the previous Dread Pirate Roberts, just as you will inherit it from me. The man I inherited it from is not the real Dread Pirate Roberts either. His name was Cummerbund. The real Roberts has been retired 15 years and living like a king in Patagonia.'
7# password = "$2$bhjy5j4njn8r5mx3yo6ksmdt9hbm8fan$myysr1gnbcf9ggpf4dzjdky3by9nbb8w9wbm7wciu97sbb7zhomy";
8
9# this one is the hash for:
10# The point is, ladies and gentleman, that greed, for lack of a better word, is good. Greed is right, greed works. Greed clarifies, cuts through, and captures the essence of the evolutionary spirit. Greed, in all of its forms; greed for life, for money, for love, knowledge has marked the upward surge of mankind. And greed, you mark my words, will not only save Teldar Paper, but that other malfunctioning corporation called the USA. Thank you very much.
11# password = "$2$eqre5picpektnop85uashzyxpxeeo6cr$gugecdr35jhg8uhzxkwh4jnp19rtxc6ukaadaad5665sm1rapmfy";
12
13# this one is the hash for:
14# whence the day goes on
15# password = "$2$am4gzwgxbuksntkn7784g6mpoir4mp1o$83i86hfju6jfbp6g9w9sh44qacqmne85q9weah6xcj1d3c5ei1rb";
16
17
18# You can list multiple bind sockets on networks and file systems.
19#bind_socket = "/var/run/rspamd/rspamd.sock mode=0666 owner=nobody";
20
21# The worker controller handles multiple functions:
22# - trains spam
23# - trains not-spam
24# - hosts the built-in statistics web interface
25# - web interface also allows config modification and data injection
26
27# The config parameter 'secure_ip' defines which sources DO NOT need a
28# password to connect to this worker controller.
29# By default, 'secure_ip' is defined as:
30# secure_ip = "127.0.0.1";
31# secure_ip = "::1";
32#
33# If you need distributed rspamd access or want to view your spam stats console
34# from another machine without SSH tunneling port 11334, you can add multiple
35# non-localhost IP address config lines here, but also remember to
36# define 'password' above as well.
37
38bind_socket = "127.0.0.1:11334";
39#bind_socket = "192.168.122.8:11334";
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/worker-normal.inc b/ansible/roles/rspamd/files/rspamd/local.d/worker-normal.inc
new file mode 100644
index 0000000..ffa77f5
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/worker-normal.inc
@@ -0,0 +1,3 @@
1# we're not running rspamd in a distributed setup, so this can be disabled
2# the proxy worker will handle all the spam filtering
3enabled = false;
diff --git a/ansible/roles/rspamd/files/rspamd/local.d/worker-proxy.inc b/ansible/roles/rspamd/files/rspamd/local.d/worker-proxy.inc
new file mode 100644
index 0000000..056d2bc
--- /dev/null
+++ b/ansible/roles/rspamd/files/rspamd/local.d/worker-proxy.inc
@@ -0,0 +1,16 @@
1# this worker will be used as postfix milter
2milter = yes;
3
4# note to self - tighten up these permissions
5#bind_socket = "/var/run/rspamd/milter.sock mode=0666 owner=nobody";
6
7# DEFAULT: listens on localhost:11332
8
9
10# the following specifies self-scan mode, for when rspamd is on the same
11# machine as postfix
12timeout = 120s;
13upstream "local" {
14 default = yes;
15 self_scan = yes;
16}
diff --git a/ansible/roles/rspamd/handlers/main.yml b/ansible/roles/rspamd/handlers/main.yml
new file mode 100644
index 0000000..de7cfc9
--- /dev/null
+++ b/ansible/roles/rspamd/handlers/main.yml
@@ -0,0 +1,11 @@
1---
2- name: restart rspamd
3 service:
4 name: rspamd
5 state: restarted
6
7- name: restart redis
8 service:
9 name: redis
10 state: restarted
11
diff --git a/ansible/roles/rspamd/meta/main.yml b/ansible/roles/rspamd/meta/main.yml
new file mode 100644
index 0000000..8ab008e
--- /dev/null
+++ b/ansible/roles/rspamd/meta/main.yml
@@ -0,0 +1,3 @@
1---
2dependencies:
3 - role: gpg
diff --git a/ansible/roles/rspamd/tasks/main.yml b/ansible/roles/rspamd/tasks/main.yml
new file mode 100644
index 0000000..a15c330
--- /dev/null
+++ b/ansible/roles/rspamd/tasks/main.yml
@@ -0,0 +1,54 @@
1---
2# Set Up rspam repo
3- name: install rspam key
4 apt_key:
5 url: https://rspamd.com/apt-stable/gpg.key
6 state: present
7
8- name: create rspam repo
9 apt_repository:
10 repo: deb http://rspamd.com/apt-stable/ bionic main
11 state: present
12
13# rspamd install and configuration
14- name: install shitdis
15 apt:
16 pkg: redis
17 state: latest
18
19- name: set guanodis directory permissions
20 file:
21 path: /var/lib/redis
22 owner: redis
23 group: redis
24 mode: 0700
25 state: directory
26
27- name: copy poodis config
28 copy:
29 src: redis/
30 dest: /etc/redis/
31 notify:
32 - restart redis
33
34- name: install rspamd
35 apt:
36 pkg: rspamd
37 state: latest
38
39- name: copy rspamd config
40 copy:
41 src: rspamd/local.d/
42 dest: /etc/rspamd/local.d/
43 notify:
44 - restart rspamd
45
46# verify everything is running
47- name: verify services are running in dependency order
48 service:
49 name: "{{ item }}"
50 enabled: yes
51 state: started
52 loop:
53 - redis
54 - rspamd
diff --git a/ansible/roles/sieve/tasks/main.yml b/ansible/roles/sieve/tasks/main.yml
new file mode 100644
index 0000000..1be8729
--- /dev/null
+++ b/ansible/roles/sieve/tasks/main.yml
@@ -0,0 +1,20 @@
1---
2- name: copy user sieves to user vmail homedirs
3 copy:
4 src: "sieve/{{ item.username }}.sieve"
5 dest: "/var/mail/vhosts/{{item.domain}}/{{item.user}}/sieve/"
6 mode: 0600
7 owner: vmail
8 group: vmail
9 loop: "{{ sieve.users }}"
10
11 # dovecot only reads .dovecot.sieve symlink in each users's virtual homedir
12- name: create active sieve symlinks for dovecot users
13 file:
14 state: link
15 src: "/var/mail/vhosts/{{item.domain}}/{{item.user}}/sieve/{{ item.username }}.sieve"
16 dest: "/var/mail/vhosts/{{item.domain}}/{{item.user}}/.dovecot.sieve"
17 owner: vmail
18 group: vmail
19 force: yes
20 loop: "{{ sieve.users }}"
diff --git a/ansible/runner.sh b/ansible/runner.sh
new file mode 100755
index 0000000..c06b35a
--- /dev/null
+++ b/ansible/runner.sh
@@ -0,0 +1,41 @@
1#!/usr/bin/env bash
2
3set -x
4set -e
5
6here=$(dirname $0)
7
8GROUP_PLAYBOOK=$1
9INVENTORY="$here/inventory/inventory"
10
11# More debug options from ansible docs about auto-provisioning...
12# (mostly disables built-in checks and overrides some defaults)
13# (broken across lines so all the options are easier to see):
14# PYTHONUNBUFFERED=1
15# ANSIBLE_FORCE_COLOR=true
16# ANSIBLE_HOST_KEY_CHECKING=false
17# ANSIBLE_SSH_ARGS='-o UserKnownHostsFile=/dev/null -o ControlMaster=auto -o ControlPersist=60s'
18# ansible-playbook
19# --private-key=/home/someone/.vagrant.d/insecure_private_key
20# --user=vagrant
21# --connection=ssh
22# --limit='machine1'
23# --inventory-file=/inventory/vagrant_ansible_inventory
24# playbook.yml
25
26# "debug" below formats output as properly indented/pretty printed.
27# You can also replace "debug" with "yaml" for a different view.
28# For details of all stdout callbacks, see:
29# https://docs.ansible.com/ansible/2.5/plugins/callback.html
30# and/or
31# ansible-doc -t callback -l
32
33# The following assumes you are testing per-host playbooks and the
34# host(s) you are testing are a prefix of the playbook name.
35# e.g. if your host name is "webby" and you test playbook "web",
36# that's a valid prefix match ("web" is a prefix of "webby")
37# or, you can use direct names: deploy to mailmash using mailmash.yml
38ANSIBLE_STDOUT_CALLBACK=debug PYTHONUNBUFFERED=1 ansible-playbook -v --inventory $INVENTORY \
39 -l $GROUP_PLAYBOOK \
40 "$here/$GROUP_PLAYBOOK.yml" \
41 --ask-pass --ask-become-pass
Powered by cgit v1.2.3 (git 2.41.0)