Initial commit

This commit is contained in:
Pijus Kamandulis 2024-05-25 23:11:17 +03:00
commit fcfd816548
11 changed files with 505 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Ansible configuration files
ansible/inventory.yml
ansible/vars.yml

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Pijus Kamandulis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

73
README.md Normal file
View File

@ -0,0 +1,73 @@
# mail-server
This repository contains the scripts, configuration files, and Ansible playbooks for my email server setup. After running my own email service for seven years and redoing the setup multiple times, I've decided to document everything here to streamline maintenance.
Feel free to read, copy, use, and suggest improvements.
## Configuration
Ansible is used for configuration. The playbooks use a `vars.yml` file for settings. This file contains sensitive information, so it is not included in the repository. Below is an example of its structure:
```yml
---
ssh_public_key: "ssh-rsa AAAAB3...ak4EsUU="
mx1_domains:
- mx1.pikami.org
mx1_mail_domain: "mx1.pikami.org"
mail_domains:
- pikami.net
- pikami.org
mail_users:
- user: bob@pikami.org
password: Password123
virtuals:
- "bob@pikami.net"
- "bob.coolman@pikami.net"
- user: alice@pikami.org
password: Password123
virtuals:
- "alice@pikami.net"
```
The hosts are taken from the `inventory.yml` file:
```yml
all:
hosts:
mx1:
ansible_host: 51.158.215.227
```
## Environment setup
Install python on remote hosts:
```sh
# replace "mx1" with server name
ansible -m raw -i inventory.yml -u root -a "pkg_add python%3.8" mx1
```
Install required ansible collections:
```sh
ansible-galaxy collection install ansible.posix
ansible-galaxy collection install community.general
```
Run playbooks
```sh
# replace "01-initial_setup.yml" with the playbook you want to run
ansible-playbook -i inventory.yml 01-initial_setup.yml
```
Current ansible playbooks:
- 01-initial_setup.yml
- applies available system patches
- upgrades all installed packages
- installs nano, curl and git
- disables ssh password logins
- adds ssh public key
- 02-ssl.yml - generates ssl certificates and adds a renew cron job
- 03-mail.yml - installs and configures dovecot and opensmtpd

View File

@ -0,0 +1,41 @@
- name: Initial System Setup
hosts: mx1
remote_user: root
become: true
become_method: su
vars_files:
- vars.yml
tasks:
- name: Apply all available system patches
command: syspatch
register: syspatch
failed_when: syspatch.rc != 0 and syspatch.rc != 2
changed_when: syspatch.rc == 0
- name: Update package list and upgrade all packages
command: pkg_add -u
- name: Install essential packages
community.general.openbsd_pkg:
name:
- nano
- curl
- git
state: present
- name: Disable SSH password authentication
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "^#?PasswordAuthentication"
line: "PasswordAuthentication no"
state: present
- name: Restart SSH service to apply changes
ansible.builtin.service:
name: sshd
state: restarted
- name: Add SSH public key to authorized_keys
ansible.posix.authorized_key:
user: root
key: "{{ ssh_public_key }}"

50
ansible/02-ssl.yml Normal file
View File

@ -0,0 +1,50 @@
- name: SSL Setup
hosts: mx1
remote_user: root
vars_files:
- vars.yml
tasks:
- name: Create vhost directories
file:
path: "/var/www/vhosts/{{ item }}"
state: directory
owner: www
with_items: "{{ mx1_domains }}"
- name: Install httpd.conf
template:
src: "templates/httpd.conf"
dest: "/etc/httpd.conf"
- name: Enable and start httpd
service:
name: httpd
enabled: yes
state: started
- name: Install acme-client.conf
template:
src: "templates/acme-client.conf"
dest: "/etc/acme-client.conf"
- name: Initial acme-client run
command: "/usr/sbin/acme-client {{ item }}"
args:
creates: "/etc/ssl/{{ item }}.fullchain.pem"
with_items: "{{ mx1_domains }}"
notify:
- reload httpd
- name: Renew certificates via root crontab
cron:
name: "acme-client renew {{ item }}"
minute: "0"
job: "sleep $((RANDOM \\% 2048)) && acme-client {{ item }} && rcctl reload httpd"
user: root
with_items: "{{ mx1_domains }}"
handlers:
- name: reload httpd
service:
name: httpd
state: reloaded

120
ansible/03-mail.yml Normal file
View File

@ -0,0 +1,120 @@
- name: OpenSMTPD Installation and Configuration
hosts: mx1
remote_user: root
vars_files:
- vars.yml
tasks:
- name: Install Packages
community.general.openbsd_pkg:
name:
- opensmtpd-filter-dkimsign
- dovecot
- dovecot-pigeonhole
- opensmtpd-extras
state: present
- name: Create the vmail group
group:
name: vmail
gid: 2000
- name: Create vmail user
user:
name: vmail
group: vmail
shell: /sbin/nologin
createhome: yes
home: /var/mail/vmail
uid: 2000
- name: Generate dkim keys
shell: |
KEYLEN=1024
DOMAIN={{ mx1_mail_domain }}
mkdir -p /etc/mail/dkim
if [ -f /etc/mail/dkim/$DOMAIN.key ]; then
echo "$DOMAIN.key already exists."
exit 0
fi
cd /etc/mail/dkim
(umask 337; openssl genrsa -out $DOMAIN.key $KEYLEN)
openssl rsa -in $DOMAIN.key -pubout -out $DOMAIN.pub
group info _dkimsign >/dev/null && chgrp _dkimsign $DOMAIN.key
echo "add the $DOMAIN.dns to the zone file"
echo "selector1._domainkey.$DOMAIN. 3600 IN TXT \"v=DKIM1; k=rsa; p=$(sed -e '1d' -e '$d' $DOMAIN.pub | tr -d '\n')\"" > ~/$DOMAIN.dns
- name: Configure OpenSMTPD smtpd.conf
template:
src: "templates/smtpd.conf"
dest: /etc/mail/smtpd.conf
notify:
- reload smtpd
- name: Enable and start OpenSMTPD service
service:
name: smtpd
enabled: yes
state: started
- name: Delete default dovecot configs
shell: |
if [ -f /etc/dovecot/conf.d/10-ssl.conf ]; then
cd /etc/dovecot/
rm -rf *
fi
- name: Install dovecot.conf
template:
src: "templates/dovecot.conf"
dest: "/etc/dovecot/dovecot.conf"
notify:
- reload dovecot
- name: Configure users
block:
- name: Remove existing
shell: |
echo "" > /etc/dovecot/users
chmod 640 /etc/dovecot/users
chown _smtpd:_dovecot /etc/dovecot/users
echo "" > /etc/mail/accounts
chmod 640 /etc/mail/accounts
chown _smtpd: /etc/mail/accounts
echo "" > /etc/mail/virtuals
chown _smtpd: /etc/mail/virtuals
- name: Add user accounts
loop: "{{ mail_users }}"
no_log: true
shell: |
DOVECOT_PASS=$(doveadm pw -p {{ item.password }})
SMTP_PASS=$(smtpctl encrypt {{ item.password }})
echo "{{ item.user }}:$DOVECOT_PASS::::" >> /etc/dovecot/users
echo "{{ item.user }}:$SMTP_PASS::::" >> /etc/mail/accounts
- name: Install dovecot.conf
template:
src: "templates/virtuals.conf"
dest: "/etc/mail/virtuals"
- name: Enable dovecot service
service:
name: dovecot
enabled: true
state: started
handlers:
- name: reload smtpd
service:
name: smtpd
state: restarted
- name: reload dovecot
service:
name: dovecot
state: reloaded

View File

@ -0,0 +1,12 @@
authority letsencrypt {
api url "https://acme-v02.api.letsencrypt.org/directory"
account key "/etc/acme/letsencrypt-privkey.pem"
}
{% for domain in mx1_domains %}
domain "{{ domain }}" {
domain key "/etc/ssl/private/{{ domain }}.key"
domain full chain certificate "/etc/ssl/{{ domain }}.fullchain.pem"
sign with letsencrypt
}
{% endfor %}

View File

@ -0,0 +1,127 @@
# Enable ssl
ssl = required
ssl_cert = < /etc/ssl/{{ mx1_mail_domain }}.fullchain.pem
ssl_key = < /etc/ssl/private/{{ mx1_mail_domain }}.key
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes
disable_plaintext_auth = yes
# Define used protocols
protocols = lmtp imap sieve
service lmtp {
unix_listener lmtp {
user = vmail
group = vmail
}
}
service imap-login {
inet_listener imap {
port = 143
}
inet_listener imaps {
port = 993
}
}
# Setup users
passdb {
driver = passwd-file
args = scheme=ARGON2ID-CRYPT username_format=%u /etc/dovecot/users
}
userdb {
driver = passwd-file
args = username_format=%u /etc/dovecot/users
override_fields = uid=vmail gid=vmail home=/var/mail/vmail/%d/%n
}
# Setup mail location
mail_location = maildir:~/Maildir
# setup auth listener
service auth {
unix_listener auth-userdb {
mode = 0660
user = vmail
group = vmail
}
}
# change the authworker to run as non-root
service auth-worker {
user = $default_internal_user
}
# setup local delivery options
quota_full_tempfail = yes
protocol lda {
mail_plugins = $mail_plugins sieve
}
# setup some common mailboxes that are used by different clients to consistent destinations
namespace inbox {
inbox = yes
mailbox Spam {
special_use = \Junk
}
mailbox "Deleted Items" {
special_use = \Trash
}
}
# setup local delivery protocol
protocol lmtp {
mail_plugins = $mail_plugins sieve
}
# disable verify quota befor replying rcpt to
lmtp_rcpt_check_quota = no
# setup stats service
service stats {
unix_listener stats-writer {
user =
group = $default_internal_group
mode = 0660
}
}
# enable mail plugins
mail_plugins = $mail_plugins notify
# setup metrics
metric auth_success {
filter = event=auth_request_finished AND success=yes
}
metric auth_failures {
filter = event=auth_request_finished AND NOT success=yes
}
metric imap_command {
filter = event=imap_command_finished
group_by = cmd_name tagged_reply_state
}
metric smtp_command {
filter = event=smtp_server_command_finished
group_by = cmd_name status_code duration:exponential:1:5:10
}
metric mail_delivery {
filter = event=mail_delivery_finsihed
group_by = duration:exponential:1:5:10
}
# enable IMAP protocol
protocol imap {
mail_plugins = $mail_plugins imap_sieve
}
# setup sieve plugin options
# enable if there needs to be default sieve processing
#plugin {
# sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve
# sieve_before = /etc/dovecot/sieve/default.sieve
# sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
# sieve_plugins = sieve_imapsieve sieve_extprograms
#}

View File

@ -0,0 +1,27 @@
server "{{ inventory_hostname }}" {
listen on * port 80
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
location * {
block return 302 "https://$HTTP_HOST$REQUEST_URI"
}
}
{% for vhost in mx1_domains %}
server "{{ vhost }}" {
listen on * tls port 443
tls {
certificate "/etc/ssl/{{ vhost }}.fullchain.pem"
key "/etc/ssl/private/{{ vhost }}.key"
}
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
location * {
root "/vhosts/{{ vhost }}"
}
}
{% endfor %}

View File

@ -0,0 +1,23 @@
pki {{ mx1_mail_domain }} cert "/etc/ssl/{{ mx1_mail_domain }}.fullchain.pem"
pki {{ mx1_mail_domain }} key "/etc/ssl/private/{{ mx1_mail_domain }}.key"
table aliases file:/etc/mail/aliases
table users passwd:/etc/mail/accounts
table virtuals file:/etc/mail/virtuals
filter dkimsign_rsa proc-exec "filter-dkimsign -d {{ mx1_mail_domain }} -s selector1 \
-k /etc/mail/dkim/{{ mx1_mail_domain }}.key" user _dkimsign group _dkimsign
listen on socket filter dkimsign_rsa
listen on all tls pki {{ mx1_mail_domain }}
listen on all port submission tls-require pki {{ mx1_mail_domain }} auth <users> filter dkimsign_rsa
listen on all port smtps tls-require pki {{ mx1_mail_domain }} auth <users> filter dkimsign_rsa
action "local_mail" lmtp "/var/dovecot/lmtp" rcpt-to virtual <virtuals>
action "outbound" relay
{% for domain in mail_domains %}
match from any for domain {{ domain }} action "local_mail"
{% endfor %}
match from local for local action "local_mail"
match from local for any action "outbound"

View File

@ -0,0 +1,8 @@
{% for user in mail_users %}
{{ user.user }}: vmail
{% if (user.virtuals is defined) and user.virtuals %}
{% for virtual in user.virtuals %}
{{ virtual }}: {{ user.user }}
{% endfor %}
{% endif %}
{% endfor %}