diff --git a/inventories/z9/host_vars/send-only-mailserver.yaml b/inventories/z9/host_vars/send-only-mailserver.yaml
new file mode 100644
index 0000000..a57c477
--- /dev/null
+++ b/inventories/z9/host_vars/send-only-mailserver.yaml
@@ -0,0 +1,8 @@
+send_only_mail_server__mail_server_fqdn: send-only-mailserver.ccchh.net
+send_only_mail_server__mail_server_fqdn_zone: ccchh.net
+send_only_mail_server__mail_domains:
+ - name: send-only-mail.ccchh.net
+ zone: ccchh.net
+send_only_mail_server__bind_9_host: authoritative-dns
+
+cert__acme_account_email: j+letsencrypt-ccchh@jsts.xyz
diff --git a/inventories/z9/hosts.yaml b/inventories/z9/hosts.yaml
index 659635d..1ba3d2f 100644
--- a/inventories/z9/hosts.yaml
+++ b/inventories/z9/hosts.yaml
@@ -27,3 +27,6 @@ all:
keycloak:
ansible_host: keycloak.z9.ccchh.net
ansible_user: chaos
+ send-only-mailserver:
+ ansible_host: send-only-mailserver.z9.ccchh.net
+ ansible_user: chaos
diff --git a/playbooks/deploy_send_only_mailserver.yaml b/playbooks/deploy_send_only_mailserver.yaml
new file mode 100644
index 0000000..3fd9667
--- /dev/null
+++ b/playbooks/deploy_send_only_mailserver.yaml
@@ -0,0 +1,6 @@
+---
+- name: Deploy the Send-Only-Mailserver
+ hosts: send-only-mailserver
+ become: true
+ roles:
+ - send_only_mail_server
diff --git a/playbooks/roles/send_only_mail_server/README.md b/playbooks/roles/send_only_mail_server/README.md
new file mode 100644
index 0000000..f391786
--- /dev/null
+++ b/playbooks/roles/send_only_mail_server/README.md
@@ -0,0 +1,35 @@
+# Role `send_only_mail_server`
+
+Makes sure a send-only mail server is deployed using OpenSMTPD and Rspamd for DKIM signing.
+
+Make sure to manually set a DMARC record and MX record for the mail domains.
+
+## Supported Distributions
+
+The following distributions are supported:
+
+- Debian 11
+
+## Required Arguments
+
+For the required arguments look at the [`argument_specs.yaml`](./meta/argument_specs.yml)
+
+Also make sure to set the following for the `cert` role dependency:
+
+- `cert__acme_account_email`
+
+## Updates
+
+This role doesn't handle updates.
+However it uses the system package manager for installing all the packages, so when you're making sure the system packages are up-to-date, you're handling updates for the packages installed by this role as well.
+
+## `hosts`
+
+The `hosts` for this role need to be the machines on which you want to deploy a mail server.
+
+## Links & Resources
+
+-
+-
+-
+-
diff --git a/playbooks/roles/send_only_mail_server/files/etc_rspamd_settings.conf b/playbooks/roles/send_only_mail_server/files/etc_rspamd_settings.conf
new file mode 100644
index 0000000..a117504
--- /dev/null
+++ b/playbooks/roles/send_only_mail_server/files/etc_rspamd_settings.conf
@@ -0,0 +1,7 @@
+dkim_signing {
+ id = "dkim_signing";
+ apply {
+ symbols_enabled = ["DKIM_SIGNED"];
+ flags = ["skip_process"]; # Disable expensive MIME processing
+ }
+}
diff --git a/playbooks/roles/send_only_mail_server/handlers/main.yaml b/playbooks/roles/send_only_mail_server/handlers/main.yaml
new file mode 100644
index 0000000..852d66f
--- /dev/null
+++ b/playbooks/roles/send_only_mail_server/handlers/main.yaml
@@ -0,0 +1,11 @@
+- name: Restart `opensmtpd.service`
+ ansible.builtin.systemd:
+ name: opensmtpd.service
+ state: restarted
+ become: true
+
+- name: Restart `rspamd.service`
+ ansible.builtin.systemd:
+ name: rspamd.service
+ state: restarted
+ become: true
diff --git a/playbooks/roles/send_only_mail_server/meta/argument_specs.yaml b/playbooks/roles/send_only_mail_server/meta/argument_specs.yaml
new file mode 100644
index 0000000..bcf83d9
--- /dev/null
+++ b/playbooks/roles/send_only_mail_server/meta/argument_specs.yaml
@@ -0,0 +1,31 @@
+argument_specs:
+ main:
+ options:
+ send_only_mail_server__mail_server_fqdn:
+ description: The FQDN of the mail server host itself.
+ type: str
+ required: true
+ send_only_mail_server__mail_server_fqdn_zone:
+ description: >
+ The DNS zone on the BIND 9 server for records for the mail server host
+ FQDN.
+ type: str
+ required: true
+ send_only_mail_server__mail_domains:
+ description: The domains the mail server should send mails for.
+ type: list
+ elements: dict
+ required: true
+ options:
+ name:
+ description: The domain name.
+ type: str
+ required: true
+ zone:
+ description: The DNS zone on the BIND 9 server.
+ type: str
+ required: true
+ send_only_mail_server__bind_9_host:
+ description: The machine running BIND 9 to deploy DNS records via.
+ type: str
+ required: true
diff --git a/playbooks/roles/send_only_mail_server/meta/main.yaml b/playbooks/roles/send_only_mail_server/meta/main.yaml
new file mode 100644
index 0000000..f730c82
--- /dev/null
+++ b/playbooks/roles/send_only_mail_server/meta/main.yaml
@@ -0,0 +1,7 @@
+dependencies: # noqa meta-no-info
+ - role: distribution_check
+ vars:
+ distribution_check__supported_distributions:
+ - name: Debian
+ versions:
+ - "11"
diff --git a/playbooks/roles/send_only_mail_server/tasks/ensure_dkim_keypair.yaml b/playbooks/roles/send_only_mail_server/tasks/ensure_dkim_keypair.yaml
new file mode 100644
index 0000000..76ad2c6
--- /dev/null
+++ b/playbooks/roles/send_only_mail_server/tasks/ensure_dkim_keypair.yaml
@@ -0,0 +1,55 @@
+- name: make sure DKIM private key exists
+ community.crypto.openssl_privatekey:
+ path: "/etc/mail-dkim/{{ item.name }}.key"
+ size: 1024
+ type: RSA
+ owner: "root"
+ group: "_rspamd"
+ mode: "0640"
+ become: true
+ notify: Restart `rspamd.service`
+
+- name: make sure DKIM public key exists
+ community.crypto.openssl_publickey:
+ path: "/etc/mail-dkim/{{ item.name }}.pub"
+ privatekey_path: "/etc/mail-dkim/{{ item.name }}.key"
+ return_content: true
+ become: true
+ notify: Restart `rspamd.service`
+ register: send_only_mail_server__dkim_public_key
+
+- name: deploy DKIM public key DNS entry # noqa: no-handler
+ delegate_to: "{{ send_only_mail_server__bind_9_host }}"
+ when: send_only_mail_server__dkim_public_key.changed
+ block:
+ - name: Add file containing nsupdate commands for removing DKIM public key TXT records
+ ansible.builtin.template:
+ src: nsupdate_delete_dkim_public_key_txt_records.j2
+ dest: /root/nsupdate_delete_dkim_public_key_txt_records
+ owner: root
+ group: root
+ mode: "0600"
+
+ - name: Remove DNS records from BIND 9 server via nsupdate # noqa: no-changed-when
+ ansible.builtin.command: /usr/bin/nsupdate -l /root/nsupdate_delete_dkim_public_key_txt_records
+
+ - name: Add file containing nsupdate commands for adding DKIM public key TXT record
+ ansible.builtin.template:
+ src: nsupdate_add_dkim_public_key_txt_record.j2
+ dest: /root/nsupdate_add_dkim_public_key_txt_record
+ owner: root
+ group: root
+ mode: "0600"
+
+ - name: Add DNS record to BIND 9 server via nsupdate # noqa: no-changed-when
+ ansible.builtin.command: /usr/bin/nsupdate -l /root/nsupdate_add_dkim_public_key_txt_record
+ always:
+ - name: Remove file containing nsupdate commands for removing DKIM public key TXT records again
+ ansible.builtin.file:
+ path: /root/nsupdate_delete_dkim_public_key_txt_records
+ state: absent
+
+ - name: Remove file containing nsupdate commands for adding DKIM public key TXT record again
+ ansible.builtin.file:
+ path: /root/nsupdate_add_dkim_public_key_txt_record
+ state: absent
diff --git a/playbooks/roles/send_only_mail_server/tasks/main.yaml b/playbooks/roles/send_only_mail_server/tasks/main.yaml
new file mode 100644
index 0000000..d88cec0
--- /dev/null
+++ b/playbooks/roles/send_only_mail_server/tasks/main.yaml
@@ -0,0 +1,65 @@
+- name: make sure packages are installed
+ ansible.builtin.apt:
+ name:
+ - opensmtpd
+ - rspamd
+ - opensmtpd-filter-rspamd
+ become: true
+
+- name: make sure certificates exist
+ ansible.builtin.include_role:
+ name: cert
+ vars:
+ cert__domains:
+ - "{{ send_only_mail_server__mail_server_fqdn }}"
+ cert__owner: root
+ cert__group: opensmtpd
+ cert__bind_9_zone: "{{ send_only_mail_server__mail_server_fqdn_zone }}"
+ cert__bind_9_host: "{{ send_only_mail_server__bind_9_host }}"
+ cert__privkey_pem_permissions: "0640"
+ cert__fullchain_pem_permissions: "0640"
+ cert__chain_pem_permissions: "0640"
+ cert__cert_pem_permissions: "0640"
+
+- name: make sure the OpenSMTPD config is deployed
+ ansible.builtin.template:
+ src: etc_smtpd.conf.j2
+ dest: /etc/smtpd.conf
+ owner: root
+ group: root
+ mode: "0600"
+ become: true
+ notify: Restart `opensmtpd.service`
+
+- name: make sure `/etc/mail-dkim` directory exists
+ ansible.builtin.file:
+ path: /etc/mail-dkim
+ state: directory
+ owner: root
+ group: root
+ mode: "755"
+ become: true
+
+- name: make sure DKIM keypairs for all domains exist
+ loop: "{{ send_only_mail_server__mail_domains }}"
+ ansible.builtin.include_tasks: ensure_dkim_keypair.yaml
+
+- name: make sure the Rspamd `dkim_signing.conf` is deployed
+ ansible.builtin.template:
+ src: etc_rspamd_dkim_signing.conf.j2
+ dest: /etc/rspamd/local.d/dkim_signing.conf
+ owner: root
+ group: root
+ mode: "0600"
+ become: true
+ notify: Restart `rspamd.service`
+
+- name: make sure the Rspamd `settings.conf` is deployed
+ ansible.builtin.copy:
+ src: etc_rspamd_settings.conf
+ dest: /etc/rspamd/local.d/settings.conf
+ owner: root
+ group: root
+ mode: "0600"
+ become: true
+ notify: Restart `rspamd.service`
diff --git a/playbooks/roles/send_only_mail_server/templates/etc_rspamd_dkim_signing.conf.j2 b/playbooks/roles/send_only_mail_server/templates/etc_rspamd_dkim_signing.conf.j2
new file mode 100644
index 0000000..8872c20
--- /dev/null
+++ b/playbooks/roles/send_only_mail_server/templates/etc_rspamd_dkim_signing.conf.j2
@@ -0,0 +1,12 @@
+allow_username_mismatch = true;
+
+use_esld = false;
+
+domain {
+{% for mail_domain in send_only_mail_server__mail_domains %}
+ {{ mail_domain.name }} {
+ path = "/etc/mail-dkim/{{ mail_domain.name }}.key";
+ selector = "key";
+ }
+{% endfor %}
+}
diff --git a/playbooks/roles/send_only_mail_server/templates/etc_smtpd.conf.j2 b/playbooks/roles/send_only_mail_server/templates/etc_smtpd.conf.j2
new file mode 100644
index 0000000..d148fc2
--- /dev/null
+++ b/playbooks/roles/send_only_mail_server/templates/etc_smtpd.conf.j2
@@ -0,0 +1,15 @@
+# Managed by Ansible.
+# This configuration enables sending emails using this server, but to not receiving any.
+
+pki {{ send_only_mail_server__mail_server_fqdn }} cert "/etc/ansible_certs/certs/{{ send_only_mail_server__mail_server_fqdn }}/fullchain.pem"
+pki {{ send_only_mail_server__mail_server_fqdn }} key "/etc/ansible_certs/certs/{{ send_only_mail_server__mail_server_fqdn }}/privkey.pem"
+
+filter "rspamd-dkim-signing" proc-exec "filter-rspamd -settings-id dkim_signing"
+
+listen on lo
+listen on eth0 smtps pki {{ send_only_mail_server__mail_server_fqdn }} auth filter "rspamd-dkim-signing"
+listen on eth0 tls-require pki {{ send_only_mail_server__mail_server_fqdn }} auth filter "rspamd-dkim-signing"
+
+action "outbound" relay helo {{ send_only_mail_server__mail_server_fqdn }}
+
+match from any auth for any action "outbound"
diff --git a/playbooks/roles/send_only_mail_server/templates/nsupdate_add_dkim_public_key_txt_record.j2 b/playbooks/roles/send_only_mail_server/templates/nsupdate_add_dkim_public_key_txt_record.j2
new file mode 100644
index 0000000..37977f9
--- /dev/null
+++ b/playbooks/roles/send_only_mail_server/templates/nsupdate_add_dkim_public_key_txt_record.j2
@@ -0,0 +1,4 @@
+debug
+zone {{ item.zone }}
+update add key._domainkey.{{ item.name }} 60 TXT v=DKIM1;k=rsa;p={{ send_only_mail_server__dkim_public_key.publickey | replace('\n', '') | replace('-----BEGIN PUBLIC KEY-----', '') | replace('-----END PUBLIC KEY-----', '') }}
+send
diff --git a/playbooks/roles/send_only_mail_server/templates/nsupdate_delete_dkim_public_key_txt_records.j2 b/playbooks/roles/send_only_mail_server/templates/nsupdate_delete_dkim_public_key_txt_records.j2
new file mode 100644
index 0000000..94695a6
--- /dev/null
+++ b/playbooks/roles/send_only_mail_server/templates/nsupdate_delete_dkim_public_key_txt_records.j2
@@ -0,0 +1,4 @@
+debug
+zone {{ item.zone }}
+update delete key._domainkey.{{ item.name }} TXT
+send