├── .gitignore
├── mail
├── sa-learn-ham.sh
├── sa-learn-spam.sh
├── dkim_signing.template.conf
├── report-spam.sieve
├── application.template.ini
├── report-ham.sieve
├── domain.template.ini
├── delete_user.sh
├── change_password.sh
├── create_user.sh
├── smtpd.template.conf
└── dovecot.template.conf
├── nginx
├── certbot_enabler
├── www_redirect
├── secure_only
├── site-templates
│ ├── index.html
│ ├── insecure.site
│ └── secure.site
├── secure
└── nginx.conf
├── env.d
├── nginx.sh
├── mail.sh
├── vpn.sh
└── general.sh
├── test
└── pebble
│ ├── certs
│ ├── localhost
│ │ ├── README.md
│ │ ├── cert.pem
│ │ └── key.pem
│ ├── README.md
│ ├── pebble.minica.pem
│ └── pebble.minica.key.pem
│ └── pebble-config.json
├── scripts
├── 001_bootstrap.sh
├── 002_shell.sh
├── 006_pf.sh
├── 003_nginx.sh
├── 004_ssl.sh
├── 007_vpn.sh
└── 005_mail.sh
├── docs
├── acknowledgements.md
└── development.md
├── vpn
├── wgclient.template.conf
├── wg_index.html
├── iked.template.conf
├── pf.template.conf
├── unbound.template.conf
└── wg_create_user.sh
├── Makefile
├── .github
└── workflows
│ └── main.yml
├── LICENSE
├── setup.sh
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | openbsd.code-workspace
4 |
--------------------------------------------------------------------------------
/mail/sa-learn-ham.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | exec /usr/local/bin/rspamc -d "${1}" learn_ham
3 |
--------------------------------------------------------------------------------
/mail/sa-learn-spam.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | exec /usr/local/bin/rspamc -d "${1}" learn_spam
3 |
--------------------------------------------------------------------------------
/nginx/certbot_enabler:
--------------------------------------------------------------------------------
1 | location ~ /\.well-known\/acme-challenge {
2 | allow all;
3 | }
4 |
--------------------------------------------------------------------------------
/nginx/www_redirect:
--------------------------------------------------------------------------------
1 | if ($host ~* www\.(.+)) {
2 | return 301 https://$1$request_uri;
3 | }
4 |
--------------------------------------------------------------------------------
/env.d/nginx.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | NGINX_LOGS=/var/log/nginx
4 | NGINX_CONF=/etc/nginx
5 | NGINX_WWW=/var/www
6 | NGINX_USER=www
7 | NGINX_GROUP=www
8 |
--------------------------------------------------------------------------------
/mail/dkim_signing.template.conf:
--------------------------------------------------------------------------------
1 | domain {
2 | {{domain}} {
3 | path = "{{dkim_private_key}}";
4 | selector = "{{dkim_selector}}";
5 | }
6 | }
--------------------------------------------------------------------------------
/nginx/secure_only:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80 default_server;
3 | listen [::]:80 default_server;
4 |
5 | server_name _;
6 | return 301 https://$host$request_uri;
7 | }
8 |
--------------------------------------------------------------------------------
/nginx/site-templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{title}}
7 |
8 |
9 |
10 | {{stub}}
11 |
12 |
13 |
--------------------------------------------------------------------------------
/mail/report-spam.sieve:
--------------------------------------------------------------------------------
1 | require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
2 |
3 | if environment :matches "imap.user" "*" {
4 | set "username" "${1}";
5 | }
6 |
7 | pipe :copy "sa-learn-spam.sh" [ "${username}" ];
8 |
--------------------------------------------------------------------------------
/test/pebble/certs/localhost/README.md:
--------------------------------------------------------------------------------
1 | # certs/localhost
2 |
3 | This directory contains an end-entity (leaf) certificate (`cert.pem`) and
4 | a private key (`key.pem`) for the Pebble HTTPS server. It includes `127.0.0.1`
5 | as an IP address SAN, and `[localhost, pebble]` as DNS SANs.
6 |
--------------------------------------------------------------------------------
/mail/application.template.ini:
--------------------------------------------------------------------------------
1 | [security]
2 | ; Access settings
3 | allow_admin_panel = Off
4 | allow_two_factor_auth = On
5 | force_two_factor_auth = Off
6 |
7 | [login]
8 | default_domain = "{{domain}}"
9 |
10 | [plugins]
11 | ; Enable plugin support
12 | enable = Off
13 |
14 | ; List of enabled plugins
15 | enabled_list = ""
16 |
--------------------------------------------------------------------------------
/scripts/001_bootstrap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # doas setup
4 | echo -n "${PINK}${BOLD}Enter your root password now to enable doas... ${NORM}"
5 | echo "echo 'permit nopass ${USER_NAME} as root' >> /etc/doas.conf" | su
6 |
7 | echo "${YELLOW}Enabling slaacd for IPv6${NORM}"
8 | doas rcctl enable slaacd
9 | doas rcctl start slaacd
10 |
--------------------------------------------------------------------------------
/docs/acknowledgements.md:
--------------------------------------------------------------------------------
1 | # Acknowledgements
2 |
3 | * Luke Smith ([lukesmith.xyz](lukesmith.xyz)) – general inspiration for self-hosting
4 | and [emailwiz](https://github.com/LukeSmithxyz/emailwiz) mail setup script in particular
5 | * Fazal Majid – heavily borrowing from his [edgewalker](https://github.com/fazalmajid/edgewalker) VPN setup script
6 |
--------------------------------------------------------------------------------
/vpn/wgclient.template.conf:
--------------------------------------------------------------------------------
1 | [Interface]
2 | PrivateKey = {{privkey}}
3 | Address = {{my_ip}}
4 | , {{my_ip6}} # IPv6
5 | DNS = {{host_ip}}
6 | , {{host_ip6}} # IPv6
7 |
8 | [Peer]
9 | PublicKey = {{hostpubkey}}
10 | AllowedIPs = 0.0.0.0/0
11 | , ::0/0 # IPv6
12 | Endpoint = {{domain}}:{{port}}
13 | PresharedKey = {{psk}}
14 | PersistentKeepalive = 300
--------------------------------------------------------------------------------
/mail/report-ham.sieve:
--------------------------------------------------------------------------------
1 | require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
2 |
3 | if environment :matches "imap.mailbox" "*" {
4 | set "mailbox" "${1}";
5 | }
6 |
7 | if string "${mailbox}" "Trash" {
8 | stop;
9 | }
10 |
11 | if environment :matches "imap.user" "*" {
12 | set "username" "${1}";
13 | }
14 |
15 | pipe :copy "sa-learn-ham.sh" [ "${username}" ];
16 |
--------------------------------------------------------------------------------
/mail/domain.template.ini:
--------------------------------------------------------------------------------
1 | imap_host = "{{mail_domain}}"
2 | imap_port = 993
3 | imap_secure = "SSL"
4 | imap_short_login = Off
5 | sieve_use = Off
6 | sieve_allow_raw = Off
7 | sieve_host = ""
8 | sieve_port = 4190
9 | sieve_secure = "None"
10 | smtp_host = "{{mail_domain}}"
11 | smtp_port = 465
12 | smtp_secure = "SSL"
13 | smtp_short_login = Off
14 | smtp_auth = On
15 | smtp_php_mail = Off
16 | white_list = ""
--------------------------------------------------------------------------------
/nginx/site-templates/insecure.site:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | listen [::]:80;
4 |
5 | server_name www.{{domain}} {{domain}};
6 |
7 | access_log /var/log/nginx/{{domain}}.access.log;
8 | error_log /var/log/nginx/{{domain}}.error.log;
9 |
10 | root /var/www/{{domain}};
11 |
12 | location / {
13 | index index.html;
14 | }
15 |
16 | include certbot_enabler;
17 | }
18 |
--------------------------------------------------------------------------------
/test/pebble/pebble-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "pebble": {
3 | "listenAddress": "0.0.0.0:14000",
4 | "managementListenAddress": "0.0.0.0:15000",
5 | "certificate": "test/pebble/certs/localhost/cert.pem",
6 | "privateKey": "test/pebble/certs/localhost/key.pem",
7 | "httpPort": 80,
8 | "tlsPort": 5001,
9 | "ocspResponderURL": "",
10 | "externalAccountBindingRequired": false
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/vpn/wg_index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WireGuard VPN setup
5 |
6 |
7 |
8 | WireGuard VPN Configuration
9 | Scan the QR code below or download the config directly
10 |
11 |

12 |
13 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/env.d/mail.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | VMAIL_USER=vmail
4 | VMAIL_ROOT=/var/vmail
5 | VMAIL_UID="$(id -ru $VMAIL_USER)"
6 | VMAIL_GID="$(id -rg $VMAIL_USER)"
7 |
8 | MAIL_CONF_DIR=/etc/mail
9 | MAIL_CONF=$MAIL_CONF_DIR/smtpd.conf
10 | CERT_DIR="/etc/letsencrypt/live/$MAIL_DOMAIN"
11 |
12 | CREDENTIALS=$MAIL_CONF_DIR/credentials
13 | VIRTUALS=$MAIL_CONF_DIR/virtuals
14 | ALIASES=$MAIL_CONF_DIR/aliases
15 |
16 | export MAIL_CONF_DIR MAIL_CONF CERT_DIR CREDENTIALS VIRTUALS ALIASES VMAIL_USER VMAIL_UID VMAIL_GID VMAIL_ROOT
--------------------------------------------------------------------------------
/vpn/iked.template.conf:
--------------------------------------------------------------------------------
1 | # IPv6
2 | ikev2 VPN6 passive ipcomp esp \
3 | from any to {{ikev2_net6}} \
4 | local {{main_if}} peer any \
5 | psk "{{password}}" \
6 | config protected-subnet ::/0 \
7 | config address {{ikev2_net6}} \
8 | config name-server {{main_if}}
9 | # IPv6 end
10 |
11 | ikev2 VPN passive ipcomp esp \
12 | from any to {{ikev2_net}} \
13 | local {{main_if}} peer any \
14 | psk "{{password}}" \
15 | config protected-subnet 0.0.0.0/0 \
16 | config address {{ikev2_net}} \
17 | config name-server {{main_if}}
18 |
--------------------------------------------------------------------------------
/nginx/site-templates/secure.site:
--------------------------------------------------------------------------------
1 | server {
2 | listen 443 ssl http2;
3 | listen [::]:443 ssl http2;
4 |
5 | server_name www.{{domain}} {{domain}};
6 |
7 | access_log /var/log/nginx/{{domain}}.access.log;
8 | error_log /var/log/nginx/{{domain}}.error.log;
9 |
10 | root /var/www/{{domain}};
11 | ssl_certificate /etc/letsencrypt/live/{{domain}}/fullchain.pem;
12 | ssl_certificate_key /etc/letsencrypt/live/{{domain}}/privkey.pem;
13 |
14 | location / {
15 | index index.html;
16 | }
17 |
18 | include secure;
19 | }
20 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | VM=testuser@testserver.test
2 | LOCAL=$(shell ifconfig $$(route get 10.10.10.10 | sed -nE '/interface:/ { s/.*interface: +([^ ]+).*$$/\1/g; p; }') | \
3 | sed -nE '/inet / { s/.*inet ([^ ]+).*$$/\1/; p; }')
4 |
5 | .PHONY: rsync ssh pebble
6 |
7 | rsync:
8 | rsync -avz ./ $(VM):openbsd-server-setup/
9 | ssh $(VM) sh -xvc \''cd openbsd-server-setup; sed -i.bak -e "/{{local}}/ s/{{local}}/$(LOCAL)/" setup.sh;'\'
10 |
11 | ssh:
12 | ssh $(VM) doas rdate pool.ntp.org
13 | ssh $(VM)
14 |
15 | pebble:
16 | pebble -config test/pebble/pebble-config.json
17 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 |
7 | jobs:
8 | archive:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Create archive
13 | run: tar -czvf openbsd-server-setup.tar.gzip --exclude ".git*" --exclude "workflow" --exclude "openbsd-server-setup.tar.gzip" ./*
14 | - name: Upload archive
15 | uses: ncipollo/release-action@v1
16 | with:
17 | allowUpdates: true
18 | tag: latest
19 | artifacts: "openbsd-server-setup.tar.gzip"
20 | token: ${{ secrets.GITHUB_TOKEN }}
21 |
--------------------------------------------------------------------------------
/vpn/pf.template.conf:
--------------------------------------------------------------------------------
1 | # Basic security settings
2 | set reassemble yes
3 | set block-policy return
4 | # set loginterface egress
5 | match in all scrub (no-df random-id max-mss 1440)
6 | block in quick from urpf-failed label uRPF
7 | pass out all modulate state
8 |
9 | # IKEv2
10 | pass on {{ikev2_if}}
11 | match out on {{main_if}} from {{ikev2_if}}:network to any nat-to ({{main_if}})
12 |
13 | # Wireguard
14 | pass on {{wg_if}}
15 | match out on {{main_if}} from {{wg_if}}:network to any nat-to ({{main_if}})
16 | block out on {{main_if}} inet proto icmp from any to {{wg_if}}:network
17 | block out on {{main_if}} inet6 proto icmp6 from any to {{wg_if}}:network # IPv6
--------------------------------------------------------------------------------
/nginx/secure:
--------------------------------------------------------------------------------
1 | include www_redirect;
2 | include certbot_enabler;
3 |
4 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
5 | add_header X-Frame-Options "DENY";
6 | add_header X-Content-Type-Options nosniff;
7 |
8 | ssl_dhparam /etc/ssl/certs/dh.pem;
9 | ssl_protocols TLSv1.2 TLSv1.3;
10 | ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
11 | ssl_prefer_server_ciphers off;
12 |
13 | ssl_stapling on;
14 | ssl_stapling_verify on;
15 |
16 | ssl_session_cache shared:SSL:50m;
17 | ssl_session_timeout 5m;
--------------------------------------------------------------------------------
/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | user www www;
2 | worker_processes 1;
3 |
4 | worker_rlimit_nofile 1024;
5 | events {
6 |
7 | worker_connections 800;
8 | }
9 |
10 | http {
11 | include mime.types;
12 | default_type application/octet-stream;
13 | charset utf-8;
14 | index index.html index.htm;
15 |
16 | log_format main '$remote_addr - $remote_user [$time_local] "$request" '
17 | '$status $body_bytes_sent "$http_referer" '
18 | '"$http_user_agent" "$http_x_forwarded_for"';
19 |
20 | access_log logs/access.log main;
21 | access_log syslog:server=unix:/dev/log,severity=notice main;
22 |
23 | keepalive_timeout 65;
24 |
25 | server_tokens off;
26 |
27 | include secure_only;
28 | include /etc/nginx/sites-enabled/*;
29 | }
30 |
--------------------------------------------------------------------------------
/mail/delete_user.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Deletes a virtual mail user
3 | # Usage: delete_user.sh
4 | ENVS="$(dirname $0)/../env.d"
5 | . "$ENVS/general.sh"
6 | . "$ENVS/mail.sh"
7 |
8 | username="$1"
9 | [ -z "$username" ] && prompt "Specify a user" "" username
10 | [ -z "$username" ] && panic "Username cannot be empty"
11 |
12 | echo "${YELLOW}Deleting user $username${NORM}"
13 |
14 | doas grep "^$username@$DOMAIN_NAME" "$CREDENTIALS" >/dev/null || panic "User '$username' not found"
15 | doas sed -i.bak -e "/$username@$DOMAIN_NAME/d" "$CREDENTIALS"
16 | doas sed -i.bak -e "/$username@$DOMAIN_NAME/d" "$VIRTUALS"
17 |
18 | doas rcctl reload dovecot || panic "Failed to reload dovecot"
19 | doas smtpctl update table credentials || panic "Failed to reload credentials"
20 | doas smtpctl update table virtuals || panic "Failed to reload virtuals"
21 |
22 | echo "${GREEN}User $username deleted${NORM}"
23 |
--------------------------------------------------------------------------------
/scripts/002_shell.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ENVS="$(dirname $0)/../env.d"
4 | . "$ENVS/general.sh"
5 |
6 | echo "${YELLOW}Downloading shell environment and utilities${NORM}"
7 | doas pkg_add vim zsh zsh-syntax-highlighting bash curl git cmake gmake g++ wget coreutils || panic "Failed to download dependencies"
8 |
9 | # set zsh as default shell
10 | chsh -s "$(which zsh)"
11 | echo "${YELLOW}Setting up shell environment${NORM}"
12 |
13 | zsh -s <<'EOF' || panic "Failed to setup shell environment"
14 | # setup shell
15 | cd $(mktemp -d)
16 | git clone --depth 1 https://github.com/d32f123/shell-environment.git
17 |
18 | cd shell-environment && ./setup.sh || exit 1
19 | source ~/.config/zsh/.zshrc
20 |
21 | # build gitstatus, as it is not built for openbsd by default
22 | cd $ZSH/custom/themes/powerlevel10k/gitstatus
23 | bash build -w
24 | EOF
25 |
26 | echo "${PURPLE}${BOLD}Run zsh and tmux to see what have been installed${NORM}" | postinstall
27 |
--------------------------------------------------------------------------------
/env.d/vpn.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | MAIN_IF="$(ls /etc/hostname.* | grep -v enc | grep -v wg | head -1 | cut -d. -f 2)"
4 | MAIN_IP="$(ifconfig $MAIN_IF | grep inet | grep -v inet6 | cut -d' ' -f2)"
5 | MAIN_IP6="$(ifconfig $MAIN_IF | grep inet6 | grep -v '%' | cut -d' ' -f2)"
6 |
7 | VPN_USER=vpn
8 |
9 | [ -z "$WG_IF" ] && WG_IF=wg0
10 | [ -z "$WG_NET" ] && {
11 | WG_IP=$(ifconfig $WG_IF | grep -E 'inet[^6]' | cut -d' ' -f 2)
12 | WG_NETMASK=$(ifconfig $WG_IF | grep -E 'inet[^6]' | sed -nE 's/^.*netmask ([^ ]+)($| .*$)/\1/p')
13 | WG_PREFIX=$(ipcalc $WG_IP / $WG_NETMASK | grep network | cut -d'/' -f2)
14 |
15 | WG_NET=$WG_IP/$WG_PREFIX
16 | }
17 | [ -z "$WG_NET6" ] && {
18 | WG_IP6=$(ifconfig $WG_IF | grep 'inet6 ' | tail -1 | cut -d' ' -f 2)
19 | WG_PREFIX6=$(ifconfig $WG_IF | grep 'inet6 ' | tail -1 | sed -nE 's/^.*prefixlen ([^ ]+)($| .*$)/\1/p')
20 | WG_NET6="$WG_IP6 $WG_PREFIX6"
21 | }
22 | [ -z "$WG_PUBKEY" ] && WG_PUBKEY=$(doas ifconfig $WG_IF | grep wgpubkey | cut -d' ' -f2)
23 | [ -z "$WG_PORT" ] && WG_PORT=$(doas ifconfig $WG_IF | grep wgport | cut -d' ' -f2)
24 |
--------------------------------------------------------------------------------
/mail/change_password.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Changes password for an already created virtual mail user
3 | # Usage: change_password.sh []
4 | ENVS="$(dirname $0)/../env.d"
5 | . "$ENVS/general.sh"
6 | . "$ENVS/mail.sh"
7 |
8 | username="$1"
9 | password="$2"
10 | [ -z "$username" ] && prompt "Specify a user" "" username
11 | [ -z "$username" ] && panic "Username cannot be empty"
12 |
13 | [ -z "$password" ] && prompt_password "Enter password for user $username:" password
14 | [ -z "$password" ] && panic "Password cannot be empty"
15 | encrypted_password=$(smtpctl encrypt "$password")
16 | unset password
17 |
18 | echo "${YELLOW}Changing password for user $username${NORM}"
19 |
20 | doas grep "^$username@$DOMAIN_NAME" "$CREDENTIALS" >/dev/null || panic "User '$username' not found"
21 | doas sed -i.bak -E "s?$username@$DOMAIN_NAME:[^:]+?$username@$DOMAIN_NAME:$encrypted_password?" "$CREDENTIALS" && echo "${GREEN}Password changed${NORM}"
22 |
23 | doas rcctl reload dovecot || panic "Failed to reload dovecot"
24 | doas smtpctl update table credentials || panic "Failed to update credentials table"
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Andrey Nesterov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/pebble/certs/README.md:
--------------------------------------------------------------------------------
1 | # certs/
2 |
3 | This directory contains a CA certificate (`pebble.minica.pem`) and a private key
4 | (`pebble.minica.key.pem`) that are used to issue a end-entity certificate (See
5 | `certs/localhost`) for the Pebble HTTPS server.
6 |
7 | To get your **testing code** to use Pebble without HTTPS errors you should
8 | configure your ACME client to trust the `pebble.minica.pem` CA certificate. Your
9 | ACME client should offer a runtime option to specify a list of root CAs that you
10 | can configure to include the `pebble.minica.pem` file.
11 |
12 | **Do not** add this CA certificate to the system trust store or in production
13 | code!!! The CA's private key is **public** and anyone can use it to issue
14 | certificates that will be trusted by a system with the Pebble CA in the trust
15 | store.
16 |
17 | To re-create all of the Pebble certificates run:
18 |
19 | minica -ca-cert pebble.minica.pem \
20 | -ca-key pebble.minica.key.pem \
21 | -domains localhost,pebble \
22 | -ip-addresses 127.0.0.1
23 |
24 | From the `test/certs/` directory after [installing
25 | MiniCA](https://github.com/jsha/minica#installation)
26 |
--------------------------------------------------------------------------------
/test/pebble/certs/pebble.minica.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDCTCCAfGgAwIBAgIIJOLbes8sTr4wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
3 | AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMTcx
4 | MjA2MTk0MjEwWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAyNGUyZGIwggEi
5 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5WgZNoVJandj43kkLyU50vzCZ
6 | alozvdRo3OFiKoDtmqKPNWRNO2hC9AUNxTDJco51Yc42u/WV3fPbbhSznTiOOVtn
7 | Ajm6iq4I5nZYltGGZetGDOQWr78y2gWY+SG078MuOO2hyDIiKtVc3xiXYA+8Hluu
8 | 9F8KbqSS1h55yxZ9b87eKR+B0zu2ahzBCIHKmKWgc6N13l7aDxxY3D6uq8gtJRU0
9 | toumyLbdzGcupVvjbjDP11nl07RESDWBLG1/g3ktJvqIa4BWgU2HMh4rND6y8OD3
10 | Hy3H8MY6CElL+MOCbFJjWqhtOxeFyZZV9q3kYnk9CAuQJKMEGuN4GU6tzhW1AgMB
11 | AAGjRTBDMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
12 | BQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAF85v
13 | d40HK1ouDAtWeO1PbnWfGEmC5Xa478s9ddOd9Clvp2McYzNlAFfM7kdcj6xeiNhF
14 | WPIfaGAi/QdURSL/6C1KsVDqlFBlTs9zYfh2g0UXGvJtj1maeih7zxFLvet+fqll
15 | xseM4P9EVJaQxwuK/F78YBt0tCNfivC6JNZMgxKF59h0FBpH70ytUSHXdz7FKwix
16 | Mfn3qEb9BXSk0Q3prNV5sOV3vgjEtB4THfDxSz9z3+DepVnW3vbbqwEbkXdk3j82
17 | 2muVldgOUgTwK8eT+XdofVdntzU/kzygSAtAQwLJfn51fS1GvEcYGBc1bDryIqmF
18 | p9BI7gVKtWSZYegicA==
19 | -----END CERTIFICATE-----
20 |
--------------------------------------------------------------------------------
/test/pebble/certs/localhost/cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDGzCCAgOgAwIBAgIIbEfayDFsBtwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
3 | AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMDcx
4 | MjA2MTk0MjEwWjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
5 | AQUAA4IBDwAwggEKAoIBAQCbFMW3DXXdErvQf2lCZ0qz0DGEWadDoF0O2neM5mVa
6 | VQ7QGW0xc5Qwvn3Tl62C0JtwLpF0pG2BICIN+DHdVaIUwkf77iBS2doH1I3waE1I
7 | 8GkV9JrYmFY+j0dA1SwBmqUZNXhLNwZGq1a91nFSI59DZNy/JciqxoPX2K++ojU2
8 | FPpuXe2t51NmXMsszpa+TDqF/IeskA9A/ws6UIh4Mzhghx7oay2/qqj2IIPjAmJj
9 | i73kdUvtEry3wmlkBvtVH50+FscS9WmPC5h3lDTk5nbzSAXKuFusotuqy3XTgY5B
10 | PiRAwkZbEY43JNfqenQPHo7mNTt29i+NVVrBsnAa5ovrAgMBAAGjYzBhMA4GA1Ud
11 | DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T
12 | AQH/BAIwADAiBgNVHREEGzAZgglsb2NhbGhvc3SCBnBlYmJsZYcEfwAAATANBgkq
13 | hkiG9w0BAQsFAAOCAQEAYIkXff8H28KS0KyLHtbbSOGU4sujHHVwiVXSATACsNAE
14 | D0Qa8hdtTQ6AUqA6/n8/u1tk0O4rPE/cTpsM3IJFX9S3rZMRsguBP7BSr1Lq/XAB
15 | 7JP/CNHt+Z9aKCKcg11wIX9/B9F7pyKM3TdKgOpqXGV6TMuLjg5PlYWI/07lVGFW
16 | /mSJDRs8bSCFmbRtEqc4lpwlrpz+kTTnX6G7JDLfLWYw/xXVqwFfdengcDTHCc8K
17 | wtgGq/Gu6vcoBxIO3jaca+OIkMfxxXmGrcNdseuUCa3RMZ8Qy03DqGu6Y6XQyK4B
18 | W8zIG6H9SVKkAznM2yfYhW8v2ktcaZ95/OBHY97ZIw==
19 | -----END CERTIFICATE-----
20 |
--------------------------------------------------------------------------------
/mail/create_user.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Creates a virtual user for mail
3 | # Usage: create_user.sh []
4 | # Environment: DOMAIN_NAME, VMAIL_USER, VMAIL_UID, VMAIL_GID, VMAIL_ROOT, CREDENTIALS, VIRTUALS
5 | ENVS="$(dirname $0)/../env.d"
6 | . "$ENVS/general.sh"
7 | . "$ENVS/mail.sh"
8 |
9 | username="$1"
10 | password="$2"
11 | [ -z "$username" ] && prompt "Specify a user" "$USER_NAME " username
12 | [ -z "$username" ] && panic "Username cannot be empty"
13 |
14 | [ -z "$password" ] && prompt_password "Enter password for user $username:" password
15 | [ -z "$password" ] && panic "Password cannot be empty"
16 | encrypted_password=$(smtpctl encrypt "$password")
17 | unset password
18 |
19 | echo "${YELLOW}Creating user $username for domain $DOMAIN_NAME${NORM}"
20 |
21 | echo "${username}@${DOMAIN_NAME}:${encrypted_password}:$VMAIL_USER:$VMAIL_UID:$VMAIL_GID:$VMAIL_ROOT/$DOMAIN_NAME/$username::userdb_mail=maildir:$VMAIL_ROOT/$DOMAIN_NAME/$username" | doas tee -a "$CREDENTIALS" >/dev/null
22 | echo "${username}@${DOMAIN_NAME}: $VMAIL_USER" | doas tee -a "$VIRTUALS" >/dev/null
23 |
24 | doas rcctl reload dovecot || panic "Failed to reload dovecot"
25 | doas smtpctl update table credentials || panic "Failed to update table 'credentials'"
26 | doas smtpctl update table virtuals || panic "Failed to update table 'virtuals'"
27 |
28 | echo "${GREEN}User created${NORM}"
29 |
--------------------------------------------------------------------------------
/mail/smtpd.template.conf:
--------------------------------------------------------------------------------
1 | pki "mail" cert "/etc/letsencrypt/live/{{mail_domain}}/fullchain.pem"
2 | pki "mail" key "/etc/letsencrypt/live/{{mail_domain}}/privkey.pem"
3 |
4 | table aliases file:/etc/mail/aliases
5 | table credentials passwd:/etc/mail/credentials
6 | table virtuals file:/etc/mail/virtuals
7 |
8 | filter check_dyndns phase connect match rdns regex { '.*\.dyn\..*', '.*\.dsl\..*' } junk
9 | filter check_rdns phase connect match !rdns junk
10 | filter check_fcrdns phase connect match !fcrdns junk
11 | filter senderscore proc-exec "filter-senderscore -junkBelow 70 -slowFactor 5000"
12 | filter rspamd proc-exec "filter-rspamd"
13 |
14 | listen on all tls pki "mail" filter { check_dyndns, check_rdns, check_fcrdns, senderscore, rspamd }
15 | listen on egress port submission tls-require pki "mail" auth filter "rspamd"
16 | listen on egress port smtps smtps pki "mail" auth filter "rspamd"
17 |
18 | action "local_mail_alias" maildir "/var/vmail/{{base_domain}}/%{dest.user:lowercase}/.Local" alias
19 | action "local_mail" maildir "/var/vmail/{{base_domain}}/%{dest.user:lowercase}/.Local" virtual
20 | action "domain_mail" maildir "/var/vmail/{{base_domain}}/%{dest.user:lowercase}" virtual
21 | action "outbound" relay
22 |
23 | match from local for domain "{{base_domain}}" action "local_mail"
24 | match from local for local action "local_mail_alias"
25 | match ! from local for domain "{{base_domain}}" action "domain_mail"
26 |
27 | match from local for any action "outbound"
28 | match auth from any for any action "outbound"
29 |
--------------------------------------------------------------------------------
/scripts/006_pf.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ssh_port=$(grep -q -E "^Port [^#]+" /etc/ssh/sshd_config && sed -nE 's/^Port ([^#]+)/\1/p' /etc/ssh/sshd_config || echo ssh)
4 |
5 | ENVS="$(dirname $0)/../env.d"
6 | . "$ENVS/general.sh"
7 |
8 | echo "${YELLOW}Protecting ssh ($ssh_port) from brute force attacks
9 | Protecting mail auth (submission) from brute force attacks
10 | Protecting imap (imaps) from brute force attacks
11 | Protecting HTTP and HTTPS (80, 443) from brute force attacks${NORM}
12 | "
13 |
14 | pf_conf="
15 | table persist
16 | block quick from
17 |
18 | pass proto tcp from any to any port $ssh_port \\
19 | flags S/SA keep state \\
20 | (max-src-conn 15, max-src-conn-rate 5/3, \\
21 | overload flush global)
22 |
23 | pass proto tcp from any to any port { submission imaps } \\
24 | flags S/SA keep state \\
25 | (max-src-conn 30, max-src-conn-rate 100/3, \\
26 | overload flush global)
27 |
28 | pass proto tcp from any to any port { www https } \\
29 | flags S/SA keep state \\
30 | (max-src-conn 100, max-src-conn-rate 100/1, \\
31 | overload flush global)
32 | "
33 |
34 | PF_CONF=/etc/pf.conf
35 | echo "$pf_conf" | doas tee -a $PF_CONF >/dev/null
36 |
37 | doas rcctl enable pf
38 |
39 | echo "${YELLOW}Making an entry in /etc/daily.local to clear old bans daily${NORM}"
40 | echo "pfctl -t bruteforce -T expire 86400" | doas tee -a /etc/daily.local >/dev/null
41 |
42 | echo "${PURPLE}${BOLD}See /etc/pf.conf for newly generated rules${NORM}" | postinstall
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Usage: ./setup.sh [bootstrap] [shell] [nginx] [ssl [--ssl-test]] [mail] [pf] [vpn]
3 | # By default runs all
4 |
5 | run_all=yes
6 | for arg in "$@"; do
7 | case "$arg" in
8 | --ssl-test) ssl_test=yes ; continue ;;
9 | esac
10 | unset run_all
11 | case "$arg" in
12 | bootstrap) run_bootstrap=yes ;;
13 | shell) run_shell=yes ;;
14 | nginx) run_nginx=yes ;;
15 | ssl) run_ssl=yes ;;
16 | mail) run_mail=yes ;;
17 | pf) run_pf=yes ;;
18 | vpn) run_vpn=yes ;;
19 | esac
20 | done
21 |
22 | # ----
23 | BASE="$(pwd)"
24 | SCRIPTS="$BASE/scripts"
25 | ENVS="$BASE/env.d"
26 | . "$ENVS/general.sh"
27 |
28 | self=$$
29 | echo -n "${YELLOW}${BOLD}"
30 | echo "User: $USER_NAME"
31 | echo "Base domain name: $DOMAIN_NAME"
32 | echo "Mail domain name: $MAIL_DOMAIN"
33 | echo "VPN domain name: $VPN_DOMAIN"
34 | echo -n "${NORM}"
35 |
36 | [ -n "$ssl_test" ] && export CERTBOT_FLAGS="--server https://{{local}}:14000/dir --no-verify-ssl"
37 | i=0
38 | for stage in bootstrap shell nginx ssl mail pf vpn
39 | do
40 | i=$((i+1))
41 | [ -n "$run_all" ] || eval "[ -n \"\$run_$stage\" ]" && {
42 | echo "${YELLOW}${BOLD}---Stage $stage---${NORM}" | postinstall | log
43 | {
44 | "$SCRIPTS/$(printf '%03d' $i)_$stage.sh" || {
45 | echo "${RED}${BOLD}[FATAL] Something went wrong here${NORM}" | postinstall
46 | kill $self
47 | }
48 | } | log
49 | echo "${YELLOW}${BOLD}---Stage $stage DONE---${NORM}" | postinstall | log
50 | }
51 | done
52 |
--------------------------------------------------------------------------------
/vpn/unbound.template.conf:
--------------------------------------------------------------------------------
1 | server:
2 | verbosity: 1
3 | log-queries: yes
4 |
5 | num-threads: {{n_threads}}
6 | num-queries-per-thread: 1024
7 |
8 | interface: 127.0.0.1
9 | interface: ::1 # IPv6
10 | interface: {{main_ip}}
11 | interface: {{main_ipv6}} # IPv6
12 | outgoing-interface: {{main_ip}}
13 | outgoing-interface: {{main_ipv6}} # IPv6
14 | prefer-ip6: yes # IPv6
15 | port: 53
16 | outgoing-range: 64
17 |
18 | hide-identity: yes
19 | hide-version: yes
20 | prefetch: yes
21 | use-caps-for-id: yes
22 |
23 | # control which clients are allowed to make (recursive) queries
24 | # to this server. Specify classless netblocks with /size and action.
25 | # By default everything is refused, except for localhost.
26 | # Choose deny (drop message), refuse (polite error reply), allow.
27 | access-control: 0.0.0.0/0 refuse
28 | access-control: 127.0.0.0/8 allow
29 | access-control: {{vpn_net}} allow
30 | access-control: ::0/0 refuse # IPv6
31 | access-control: ::1 allow # IPv6
32 | access-control: {{vpn_net6}} allow # IPv6
33 |
34 | logfile: "/unbound.log"
35 | use-syslog: no
36 | pidfile: "/unbound.pid"
37 |
38 | forward-zone:
39 | name: "."
40 | forward-addr: 1.0.0.1 #one.one.one.one
41 | forward-addr: 1.1.1.1 #one.one.one.one
42 | forward-addr: 2606:4700:4700::1111 #one.one.one.one # IPv6
43 | forward-addr: 2606:4700:4700::1001 #one.one.one.one # IPv6
44 | forward-addr: 8.8.4.4 #dns.google
45 | forward-addr: 8.8.8.8 #dns.google
46 | forward-addr: 2001:4860:4860::8888 # google DNS # IPv6
47 | forward-addr: 2001:4860:4860::8844 # google DNS # IPv6
48 | forward-addr: 9.9.9.9 #dns.quad9.net
49 | forward-addr: 149.112.112.112 #dns.quad9.net
50 |
--------------------------------------------------------------------------------
/test/pebble/certs/localhost/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEowIBAAKCAQEAmxTFtw113RK70H9pQmdKs9AxhFmnQ6BdDtp3jOZlWlUO0Blt
3 | MXOUML5905etgtCbcC6RdKRtgSAiDfgx3VWiFMJH++4gUtnaB9SN8GhNSPBpFfSa
4 | 2JhWPo9HQNUsAZqlGTV4SzcGRqtWvdZxUiOfQ2TcvyXIqsaD19ivvqI1NhT6bl3t
5 | redTZlzLLM6Wvkw6hfyHrJAPQP8LOlCIeDM4YIce6Gstv6qo9iCD4wJiY4u95HVL
6 | 7RK8t8JpZAb7VR+dPhbHEvVpjwuYd5Q05OZ280gFyrhbrKLbqst104GOQT4kQMJG
7 | WxGONyTX6np0Dx6O5jU7dvYvjVVawbJwGuaL6wIDAQABAoIBAGW9W/S6lO+DIcoo
8 | PHL+9sg+tq2gb5ZzN3nOI45BfI6lrMEjXTqLG9ZasovFP2TJ3J/dPTnrwZdr8Et/
9 | 357YViwORVFnKLeSCnMGpFPq6YEHj7mCrq+YSURjlRhYgbVPsi52oMOfhrOIJrEG
10 | ZXPAwPRi0Ftqu1omQEqz8qA7JHOkjB2p0i2Xc/uOSJccCmUDMlksRYz8zFe8wHuD
11 | XvUL2k23n2pBZ6wiez6Xjr0wUQ4ESI02x7PmYgA3aqF2Q6ECDwHhjVeQmAuypMF6
12 | IaTjIJkWdZCW96pPaK1t+5nTNZ+Mg7tpJ/PRE4BkJvqcfHEOOl6wAE8gSk5uVApY
13 | ZRKGmGkCgYEAzF9iRXYo7A/UphL11bR0gqxB6qnQl54iLhqS/E6CVNcmwJ2d9pF8
14 | 5HTfSo1/lOXT3hGV8gizN2S5RmWBrc9HBZ+dNrVo7FYeeBiHu+opbX1X/C1HC0m1
15 | wJNsyoXeqD1OFc1WbDpHz5iv4IOXzYdOdKiYEcTv5JkqE7jomqBLQk8CgYEAwkG/
16 | rnwr4ThUo/DG5oH+l0LVnHkrJY+BUSI33g3eQ3eM0MSbfJXGT7snh5puJW0oXP7Z
17 | Gw88nK3Vnz2nTPesiwtO2OkUVgrIgWryIvKHaqrYnapZHuM+io30jbZOVaVTMR9c
18 | X/7/d5/evwXuP7p2DIdZKQKKFgROm1XnhNqVgaUCgYBD/ogHbCR5RVsOVciMbRlG
19 | UGEt3YmUp/vfMuAsKUKbT2mJM+dWHVlb+LZBa4pC06QFgfxNJi/aAhzSGvtmBEww
20 | xsXbaceauZwxgJfIIUPfNZCMSdQVIVTi2Smcx6UofBz6i/Jw14MEwlvhamaa7qVf
21 | kqflYYwelga1wRNCPopLaQKBgQCWsZqZKQqBNMm0Q9yIhN+TR+2d7QFjqeePoRPl
22 | 1qxNejhq25ojE607vNv1ff9kWUGuoqSZMUC76r6FQba/JoNbefI4otd7x/GzM9uS
23 | 8MHMJazU4okwROkHYwgLxxkNp6rZuJJYheB4VDTfyyH/ng5lubmY7rdgTQcNyZ5I
24 | majRYQKBgAMKJ3RlII0qvAfNFZr4Y2bNIq+60Z+Qu2W5xokIHCFNly3W1XDDKGFe
25 | CCPHSvQljinke3P9gPt2HVdXxcnku9VkTti+JygxuLkVg7E0/SWwrWfGsaMJs+84
26 | fK+mTZay2d3v24r9WKEKwLykngYPyZw5+BdWU0E+xx5lGUd3U4gG
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/test/pebble/certs/pebble.minica.key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpAIBAAKCAQEAuVoGTaFSWp3Y+N5JC8lOdL8wmWpaM73UaNzhYiqA7ZqijzVk
3 | TTtoQvQFDcUwyXKOdWHONrv1ld3z224Us504jjlbZwI5uoquCOZ2WJbRhmXrRgzk
4 | Fq+/MtoFmPkhtO/DLjjtocgyIirVXN8Yl2APvB5brvRfCm6kktYeecsWfW/O3ikf
5 | gdM7tmocwQiBypiloHOjdd5e2g8cWNw+rqvILSUVNLaLpsi23cxnLqVb424wz9dZ
6 | 5dO0REg1gSxtf4N5LSb6iGuAVoFNhzIeKzQ+svDg9x8tx/DGOghJS/jDgmxSY1qo
7 | bTsXhcmWVfat5GJ5PQgLkCSjBBrjeBlOrc4VtQIDAQABAoIBAQCAoRoou6C0ZEDU
8 | DScyN8TrvlcS0LzClaWYFFmRT5/jxOG1cr8l3elwNXpgYQ2Hb6mvim2ajHxVQg/e
9 | oxlYwO4jvWhSJzg63c0DPjS5LAlCNO6+0Wlk2RheSPGDhLlAoPeZ10YKdS1dis5B
10 | Qk4Fl1O0IHlOBCcEzV4GzPOfYDI+X6/f4xY7qz1s+CgoIxjIeiG+1/WpZQpYhobY
11 | 7CfSDdYDKtksXi7iQkc5earUAHBqZ1gQTq6e5LVm9AjRzENhMctFgcPs5zOjp2ak
12 | PluixrA8LTAfu9wQzvxDkPl0UarZVxCerw6nlAziILpQ+U6PtoPZj49VpntTc+cq
13 | 1qjzkbhBAoGBANElJmFWY2X6LgBpszeqt0ZOSbkFg2bC0wHCJrMlRzUMEn83w9e8
14 | Z2Fqml9eCC5qxJcyxWDVQeoAX6090m0qgP8xNmGdafcVic2cUlrqtkqhhst2OHCO
15 | MCQEB7cdsjiidNNrOgLbQ3i1bYID8BVLf/TDhEbRgvTewDaz6XPdoSIRAoGBAOLg
16 | RuOec5gn50SrVycx8BLFO8AXjXojpZb1Xg26V5miz1IavSfDcgae/699ppSz+UWi
17 | jGMFr/PokY2JxDVs3PyQLu7ahMzyFHr16Agvp5g5kq056XV+uI/HhqLHOWSQ09DS
18 | 1Vrj7FOYpKRzge3/AC7ty9Vr35uMiebpm4/CLFVlAoGALnsIJZfSbWaFdLgJCXUa
19 | WDir77/G7T6dMIXanfPJ+IMfVUCqeLa5bxAHEOzP+qjl2giBjzy18nB00warTnGk
20 | y5I/WMBoPW5++sAkGWqSatGtKGi0sGcZUdfHcy3ZXvbT6eyprtrWCuyfUsbXQ5RM
21 | 8rPFIQwNA6jBpSak2ohF+FECgYEAn+6IKncNd6pRfnfmdSvf1+uPxkcUJZCxb2xC
22 | xByjGhvKWE+fHkPJwt8c0SIbZuJEC5Gds0RUF/XPfV4roZm/Yo9ldl02lp7kTxXA
23 | XtzxIP8c5d5YM8qD4l8+Csu0Kq9pkeC+JFddxkRpc8A1TIehInPhZ+6mb6mvoMb3
24 | MW0pAX0CgYATT74RYuIYWZvx0TK4ZXIKTw2i6HObLF63Y6UwyPXXdEVie/ToYRNH
25 | JIxE1weVpHvnHZvVD6D3yGk39ZsCIt31VvKpatWXlWBm875MbBc6kuIGsYT+mSSj
26 | y9TXaE89E5zfL27nZe15QLJ+Xw8Io6PMLZ/jtC5TYoEixSZ9J8v6HA==
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/env.d/general.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | prompt() {
4 | prompt="$1"
5 | default="$2"
6 | result="$3"
7 | echo "${PURPLE}${BOLD}$prompt [$default] ${NORM}\c"
8 | read "$result"
9 | [ -z "$(eval echo \$$result)" ] && eval "$result=$default"
10 | }
11 |
12 | prompt_bool() {
13 | prompt="$1"
14 | default="$2"
15 | echo "${PURPLE}${BOLD}$prompt (y/n) [$default] ${NORM}\c"
16 | read res
17 | case "${res:-$default}" in
18 | y | yes | Y | YES) return 0;;
19 | esac
20 | return 1
21 | }
22 |
23 | prompt_password() {
24 | prompt="$1"
25 | result="$2"
26 | echo "${PURPLE}${BOLD}$prompt ${NORM}\c"
27 | stty -echo
28 | read "$result"
29 | stty echo
30 | echo
31 | }
32 |
33 | panic() {
34 | msg="$1"
35 | echo "${RED}${BOLD}$msg${NORM}"
36 | exit 1
37 | }
38 |
39 | log_file() {
40 | exec 3>&1
41 | tee /dev/fd/3 | perl -pe 's/\033[^m]+m//g' >>"$1" # strip control characters
42 | exec 3>&-
43 | }
44 |
45 | postinstall() {
46 | log_file "$POSTINSTALL"
47 | }
48 |
49 | log() {
50 | log_file "$LOGS"
51 | }
52 |
53 | # Colors
54 | RED="\033[0;31m"
55 | YELLOW="\033[0;33m"
56 | BOLD="\033[1m"
57 | PURPLE="\033[0;35m"
58 | GREEN="\033[0;32m"
59 | NORM="\033[0m"
60 | export RED YELLOW BOLD PURPLE GREEN NORM
61 |
62 | [ -z "$USER_NAME" ] && USER_NAME="$(whoami)"
63 | [ -z "$DOMAIN_NAME" ] && DOMAIN_NAME="$(hostname | cut -d. -f2-)"
64 | [ -z "$MAIL_DOMAIN" ] && MAIL_DOMAIN="mail.$DOMAIN_NAME"
65 | [ -z "$VPN_DOMAIN" ] && VPN_DOMAIN="vpn.$DOMAIN_NAME"
66 | NGINX_DOMAINS="$DOMAIN_NAME $MAIL_DOMAIN $VPN_DOMAIN"
67 | export USER_NAME DOMAIN_NAME MAIL_DOMAIN VPN_DOMAIN NGINX_DOMAINS
68 |
69 | [ -z "$POSTINSTALL" ] && POSTINSTALL="$(dirname $0)/post-install.txt"
70 | [ -z "$LOGS" ] && LOGS="$(dirname $0)/logs.txt"
71 | export POSTINSTALL LOGS
72 |
--------------------------------------------------------------------------------
/scripts/003_nginx.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ENVS="$(dirname $0)/../env.d"
4 | . "$ENVS/general.sh"
5 |
6 | echo "${YELLOW}Downloading dependencies${NORM}"
7 | doas pkg_add nginx || panic "Failed to download dependencies"
8 |
9 | doas rcctl enable nginx
10 |
11 | . "$ENVS/nginx.sh"
12 |
13 | echo "${YELLOW}Setting up $NGINX_LOGS directory${NORM}"
14 | doas mkdir $NGINX_LOGS
15 | doas chown ${NGINX_USER}:${NGINX_GROUP} $NGINX_LOGS
16 |
17 | echo "${YELLOW}Setting up $NGINX_CONF directory${NORM}"
18 | doas mkdir $NGINX_CONF/{sites-available,sites-enabled}
19 |
20 | doas cp nginx/{nginx.conf,secure,secure_only,www_redirect,certbot_enabler} $NGINX_CONF/
21 | doas chown ${NGINX_USER}:${NGINX_GROUP} $NGINX_CONF/{nginx.conf,secure,secure_only,www_redirect,certbot_enabler}
22 |
23 | echo "${YELLOW}Configuring NGINX for the following sites: $NGINX_DOMAINS${NORM}"
24 | for site in $NGINX_DOMAINS; do
25 | doas mkdir $NGINX_WWW/$site
26 | doas chown ${NGINX_USER}:${NGINX_GROUP} $NGINX_WWW/$site
27 |
28 | sed -E "s/{{domain}}/${site}/g" nginx/site-templates/insecure.site | doas tee $NGINX_CONF/sites-available/${site}.insecure.site >/dev/null
29 | sed -E "s/{{domain}}/${site}/g" nginx/site-templates/secure.site | doas tee $NGINX_CONF/sites-available/${site}.secure.site >/dev/null
30 |
31 | doas ln -s -f $NGINX_CONF/{sites-available,sites-enabled}/${site}.insecure.site
32 | done
33 |
34 | echo "${YELLOW}Generating index.html for site $DOMAIN_NAME${NORM}"
35 | sed -e "s/{{title}}/$DOMAIN_NAME/;
36 | s?{{stub}}?Hello there! Edit me at $NGINX_WWW/$DOMAIN_NAME/index.html?;
37 | " nginx/site-templates/index.html | doas tee $NGINX_WWW/$DOMAIN_NAME/index.html >/dev/null
38 |
39 | echo "${YELLOW}Reloading nginx with sites' configurations${NORM}"
40 | doas rcctl restart nginx || panic "Could not start nginx with the new configuration"
41 |
42 | echo "${PURPLE}${BOLD}Nginx configuration is available at $NGINX_CONF
43 | Websites serve roots are available at $NGINX_WWW
44 | Use \`doas rcctl reload nginx\` to reload nginx${NORM}" | postinstall
--------------------------------------------------------------------------------
/scripts/004_ssl.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ENVS="$(dirname $0)/../env.d"
4 | . "$ENVS/general.sh"
5 |
6 | echo "${YELLOW}Downloading dependencies${NORM}"
7 | doas pkg_add certbot || panic "Failed to download dependencies"
8 |
9 | . "$ENVS/nginx.sh"
10 |
11 | prompt_bool "Generate custom DH params for SSL? It will take about 10 minutes" "n" && {
12 | [ ! -f /etc/ssl/certs/dh.pem ] && {
13 | echo "${YELLOW}Generating DH, please wait a few minutes${NORM}"
14 | doas mkdir -p /etc/ssl/certs
15 | cd /etc/ssl/certs
16 | doas openssl dhparam -out dh.pem 4096
17 | cd -
18 | } || {
19 | echo "${YELLOW}DH already generated, to regenerate, remove /etc/ssl/certs/dh.pem${NORM}"
20 | }
21 | } || {
22 | echo "${YELLOW}Disabling custom DH params${NORM}"
23 | doas sed -i.bak -E -e 's/(^ssl_dhparam .*$)/# \1/' $NGINX_CONF/secure
24 | }
25 |
26 | prompt_bool "Enable HSTS preload? Read more at https://hstspreload.org/" "n" && {
27 | echo "${YELLOW}Enabling HSTS Preloading${NORM}"
28 | doas sed -i.bak -e 's/includeSubDomains"/includeSubDomains; preload"/' $NGINX_CONF/secure
29 | }
30 |
31 | echo "${YELLOW}Getting SSL certificates, switching to secure sites${NORM}"
32 | for site in $NGINX_DOMAINS; do
33 | doas certbot certonly -n --webroot --agree-tos -m "$USER_NAME@$DOMAIN_NAME" -d "$site,www.$site" -w "$NGINX_WWW/$site" $CERTBOT_FLAGS || {
34 | echo "${RED}Failed to get certificate for $site${NORM}"
35 | continue
36 | }
37 | success=yes
38 | doas ln -s -f $NGINX_CONF/{sites-available,sites-enabled}/${site}.secure.site
39 | doas rm $NGINX_CONF/sites-enabled/${site}.insecure.site
40 | done
41 | [ -n "$success" ] && doas rcctl reload nginx || panic "Failed to load nginx with the new configuration"
42 |
43 | echo "${YELLOW}Setting an entry to /etc/monthly.local to update certificates monthly${NORM}"
44 | echo "$(which certbot) renew --quiet --force-renewal --post-hook '/etc/rc.d/nginx reload'" | doas tee -a /etc/monthly.local >/dev/null
45 |
46 | echo "${PURPLE}${BOLD}Certificates are available at /etc/letsencrypt/live${NORM}" | postinstall
47 |
--------------------------------------------------------------------------------
/vpn/wg_create_user.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Creates a new WireGuard VPN client
3 | # Usage: wg_create_user.sh
4 | # Script will spew out a QR Code and a link to the WireGuard configuration.
5 | # The link will be invalidated at midnight by a cronjob which is set up during 007_vpn.sh
6 | usage="Usage: wg_create_user.sh "
7 |
8 | d="$(dirname $0)"
9 | ENVS="$d/../env.d"
10 | . "$ENVS/general.sh"
11 |
12 | username="$1"
13 | [ -z "$username" ] && panic "$usage"
14 |
15 | . "$ENVS/vpn.sh"
16 |
17 | echo "${YELLOW}Creating user $username for $VPN_DOMAIN${NORM}"
18 |
19 | www_secret="$(openssl rand -base64 32 | ghead -c 20)"
20 | key="$(wg genkey)"
21 | psk="$(wg genpsk)"
22 | pubkey="$(echo $key | wg pubkey)"
23 |
24 | peers=$(doas wg show $WG_IF peers | wc -l)
25 | my_suffix=$(($peers + 1))
26 | base="$(echo $WG_NET | sed -Ee 's?^.*\.([^.]+)/.*$?\1?')"
27 | base6="$(echo $WG_NET6 | sed -Ee 's/^.*:([^ /]+) .*$/\1/')"
28 | my_base=$(($base + $my_suffix))
29 | my_base6="$(printf '%x' $(($base6 + $my_suffix)))"
30 |
31 | my_ip=$(echo $WG_NET | sed -E "s?^(.*\\.)([^.]+)/.*\$?\\1$my_base/32?")
32 | my_ip6=$(echo $WG_NET6 | sed -E "s?^(.*:)([^ /]+) .*\$?\\1$my_base6/128?")
33 |
34 | IFCONFIG_WGAIPS="wgaip $my_ip"
35 | [ -n "$MAIN_IP6" ] && IFCONFIG_WGAIPS="$IFCONFIG_WGAIPS wgaip $my_ip6"
36 | echo "wgpeer $pubkey $IFCONFIG_WGAIPS wgpsk $psk # user: $username" | doas tee -a /etc/hostname.$WG_IF >/dev/null
37 | doas ifconfig $WG_IF wgpeer "$pubkey" $IFCONFIG_WGAIPS wgpsk "$psk"
38 |
39 | WWW_SECRET_ROOT=/var/www/$VPN_DOMAIN/$www_secret
40 | doas mkdir -p $WWW_SECRET_ROOT
41 | { [ -n "$MAIN_IP6" ] && cat || sed -e '/ # IPv6/ s/^.*$//;'; } <$d/wgclient.template.conf | \
42 | sed -e "s?{{privkey}}?$key?g;
43 | s?{{my_ip}}?$my_ip?g;
44 | s?{{host_ip}}?$MAIN_IP?g;
45 | s?{{hostpubkey}}?$WG_PUBKEY?g;
46 | s?{{psk}}?$psk?g;
47 | s?{{domain}}?$VPN_DOMAIN?g;
48 | s?{{port}}?$WG_PORT?g;
49 | s?{{my_ip6}}?$my_ip6?g;
50 | s?{{host_ip6}}?$MAIN_IP6?g;
51 | s/ # IPv6.*$//g;" | sed -e '/$/ {s///; N; s/\n/ /; }' | doas tee $WWW_SECRET_ROOT/wgclient.conf >/dev/null
52 |
53 | doas cat $WWW_SECRET_ROOT/wgclient.conf | doas qrencode -o $WWW_SECRET_ROOT/wgclient.png -t PNG
54 | doas cp $d/wg_index.html $WWW_SECRET_ROOT/index.html
55 | doas chmod -R o-rwx $WWW_SECRET_ROOT
56 |
57 | echo "${GREEN}VPN Configuration created!
58 | ${PURPLE}Download it at https://$VPN_DOMAIN/$www_secret/
59 | or use the QR code below:"
60 |
61 | qrencode -o - -t UTF8 "https://$VPN_DOMAIN/$www_secret/index.html"
62 |
63 | echo -n "${NORM}"
--------------------------------------------------------------------------------
/mail/dovecot.template.conf:
--------------------------------------------------------------------------------
1 | ssl = required
2 | ssl_cert =
20 | lookup file bind
21 | ```
22 | 6. Enable sshd and set up a connection to testuser.
23 | 7. `pkg_add rsync`.
24 | 8. Reboot the VM for good measure.
25 | 3. Add the same entry to `/etc/hosts` on the Host as in (2.4)
26 | 4. Run `make rsync`. This will send this whole directory to the VM and do a replace in [setup.sh](./setup.sh) that allows the VM to target the Host when requesting SSL certificates via Certbot.
27 | 5. Spin up Pebble (stub certificate server) by running `make pebble` on the Host machine. See [test/pebble/](./test/pebble) for more info.
28 | 6. SSH into the VM and run the script. You might want to test the stages one by one by running `./setup.sh `. **Note:** when running stage SSL, be sure to pass `--ssl-test` flag to target local Pebble server.
29 |
30 | **Note!** If you are using snapshots, the time will go terribly wrong on the VM. To fix it: `rdate pool.ntp.org`
31 | Use `make ssh` to do `rdate` and ssh to the VM in one go (requires `doas` already being set up)
32 |
33 | ## Repo structure
34 |
35 | - README.md – usage instructions, general information about the scripts
36 | - Makefile – contains targets that ease development
37 | - setup.sh – main file that launches the scripts corresponding to particular Stages (see Stages section)
38 | - scripts/ – contains scripts for particular stages.
39 | - env.d/ – contains environment variables and aux functions used by different scripts.
40 | - mail/ - contains configuration templates for dovecot, smtpd et c. Also contains scripts that allow creating new users, deleting existing users and changing passwords.
41 | - nginx/ – contains configuration templates for nginx and site templates
42 | - vpn/ – contains configuration templates for IKEd, WireGuard.
43 | - vpn/wg_create_user.sh – creates additional WireGuard users
44 | - test/ - contains configuration files needed for local development and testing
--------------------------------------------------------------------------------
/scripts/007_vpn.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ENVS="$(dirname $0)/../env.d"
4 | . "$ENVS/general.sh"
5 |
6 | echo "${YELLOW}Downloading VPN dependencies${NORM}"
7 | doas pkg_add base64 libqrencode wireguard-tools ipcalc coreutils || panic "Failed to download dependencies"
8 |
9 | SYSCTL_CONF=/etc/sysctl.conf
10 | IKED_CONF=/etc/iked.conf
11 | . "$ENVS/vpn.sh"
12 |
13 | echo "${YELLOW}Configuring VPN on $MAIN_IF. IP: $MAIN_IP. IPv6: ${MAIN_IP6:-}. VPN user: $VPN_USER${NORM}"
14 |
15 | sysctlset() {
16 | option="$1"
17 | value="$2"
18 |
19 | doas sysctl "$option=$value"
20 | grep "^$option" $SYSCTL_CONF && \
21 | doas sed -E -i.bak -e "s/^$option=[[:>:]]/$option=$value/" $SYSCTL_CONF || \
22 | {
23 | echo "$option=$value # for VPN" | doas tee -a $SYSCTL_CONF >/dev/null ;
24 | }
25 | }
26 |
27 | sysctlset net.inet.ip.forwarding 1
28 | sysctlset net.inet6.ip6.forwarding 1
29 | sysctlset net.inet.esp.enable 1
30 | sysctlset net.inet.esp.udpencap 1
31 | sysctlset net.inet.ah.enable 1
32 | sysctlset net.inet.ipcomp.enable 1
33 |
34 | echo "${YELLOW}Configuring Unbound DNS server${NORM}"
35 |
36 | UNBOUND_USER=_unbound
37 | UNBOUND_GROUP=_unbound
38 | UNBOUND_ROOT=/var/unbound
39 | UNBOUND_ETC=$UNBOUND_ROOT/etc
40 | UNBOUND_DB=$UNBOUND_ROOT/db
41 | UNBOUND_CONF=$UNBOUND_ETC/unbound.conf
42 |
43 | doas mkdir -p $UNBOUND_ETC $UNBOUND_DB
44 |
45 | doas unbound-anchor
46 | doas cp $UNBOUND_ETC/{unbound.conf,unbound.conf.bak}
47 |
48 | BASE_VPN_NET=10.0.0.0/8
49 | BASE_VPN_NET6=fc00:dead:beef::/48
50 |
51 | IKEV2_VPN_IF=enc0
52 |
53 | IKEV2_VPN_NET=10.1.0.0/16
54 | IKEV2_VPN_ADDR=10.1.0.1
55 | IKEV2_VPN_NTMSK=255.255.0.0
56 | IKEV2_VPN_BRDCST=10.1.255.255
57 |
58 | IKEV2_VPN_ADDR6=fc00:dead:beef:2000::1/52
59 | IKEV2_VPN_NET6=fc00:dead:beef:2000::/52
60 |
61 | WG_IF=wg0
62 | WG_PORT=51820
63 | WG_NET=10.2.0.1/16
64 | WG_NET6="fc00:dead:beef:1000::1 52"
65 |
66 | N_THREADS=$(doas sysctl | grep hw.ncpu= | cut -d= -f2)
67 |
68 | { [ -n "$MAIN_IP6" ] && cat || sed -e '/# IPv6/d;'; } /dev/null
75 |
76 | echo "${YELLOW}Enabling and starting Unbound DNS server${NORM}"
77 | doas chown $UNBOUND_USER:$UNBOUND_GROUP $UNBOUND_ROOT
78 | doas rcctl enable unbound
79 | doas rcctl restart unbound || panic "Failed to start Unbound with the new configuration"
80 |
81 | prompt_bool "Set up IKEv2? It is less secure than WireGuard" "n" && {
82 | DO_IKEV2="yes"
83 | echo "${YELLOW}Configuring IKEv2 virtual interface $IKEV2_VPN_IF${NORM}"
84 |
85 | echo "inet $IKEV2_VPN_ADDR $IKEV2_VPN_NTMSK $IKEV2_VPN_BRDCST" | doas tee /etc/hostname.$IKEV2_VPN_IF >/dev/null
86 | [ -n "$MAIN_IP6" ] && { echo "inet6 $IKEV2_VPN_ADDR6" | doas tee -a /etc/hostname.$IKEV2_VPN_IF >/dev/null ; }
87 | echo "up" | doas tee -a /etc/hostname.$IKEV2_VPN_IF >/dev/null
88 |
89 | doas sh /etc/netstart
90 |
91 | prompt_password "Enter password for IKEv2 VPN:" password
92 | { [ -n "$MAIN_IP6" ] && cat || sed -e '/# IPv6/,/# IPv6 end/d;'; } /dev/null
97 | unset password
98 | doas chmod 600 $IKED_CONF
99 |
100 | echo "${YELLOW}Enabling and starting IKEd service${NORM}"
101 | doas rcctl enable iked
102 | doas rcctl restart iked || panic "Failed to start IKEd with the new configuration"
103 |
104 | echo "${PURPLE}${BOLD}IKEv2 is available at $VPN_DOMAIN (Server Address and Remote ID)${NORM}" | postinstall
105 | }
106 |
107 | echo "${YELLOW}Configuring WireGuard VPN interface $WG_IF${NORM}"
108 | echo "$WG_NET wgport $WG_PORT wgkey $(openssl rand -base64 32)" | doas tee /etc/hostname.$WG_IF >/dev/null
109 | [ -n "$MAIN_IP6" ] && echo "inet6 $WG_NET6" | doas tee -a /etc/hostname.$WG_IF >/dev/null
110 | doas chmod 600 /etc/hostname.$WG_IF
111 |
112 | echo "${YELLOW}Restarting machine networking${NORM}"
113 | doas sh /etc/netstart || panic "Failed to restart network services"
114 | WG_PUBKEY="$(doas ifconfig $WG_IF | grep wgpubkey | cut -d' ' -f2)"
115 |
116 | echo "${YELLOW}Configuring Packet Filter${NORM}"
117 |
118 | { [ -n "$DO_IKEV2" ] && cat || sed -e '/ikev2/d'; } /dev/null
123 |
124 | echo "${YELLOW}Restarting Packet Filter${NORM}"
125 | doas pfctl -f /etc/pf.conf || panic "Failed to start pf with the new configuration"
126 |
127 | echo "${YELLOW}Creating a default user for WireGuard VPN${NORM}"
128 | export MAIN_IF MAIN_IP MAIN_IP6 WG_IF WG_NET WG_NET6 WG_PUBKEY WG_PORT
129 | vpn/wg_create_user.sh $USER_NAME | postinstall
130 |
131 | echo "${YELLOW}Creating an entry in /etc/daily.local to clear up VPN configurations available on https://$VPN_DOMAIN/ daily${NORM}"
132 | echo "ls -d /var/www/$VPN_DOMAIN/*/ | xargs rm -rf" | doas tee -a /etc/daily.local >/dev/null
133 |
134 | echo "${PURPLE}${BOLD}You can create additional users for WireGuard by running ./vpn/wg_create_user.sh${NORM}" | postinstall
135 |
136 | # TODO: Set up OpenVPN instead of IKeV2
137 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Instructions and files to set up a functional OpenBSD server
2 |
3 | This collection of scripts will set up a Web server with SSL certificates, Mail server with anti-spoofing measures, and a VPN.
4 | Pure shell scripts + config files, no unneeded dependencies.
5 |
6 | **Note:** IPv4 only and IPv4+IPv6 setups are supported.
7 | IPv6 only **WILL NOT** work. You can still use this repo as a reference though.
8 |
9 | ## Stack
10 | * Shell: zsh, oh-my-zsh, tmux
11 | * SSH
12 | * Web server – nginx with automatic http to https redirect and A+ SSL
13 | * Mail server – OpenSMTPD, Dovecot, Rspamd, Redis, RainLoop (optional, pulls PHP)
14 | * Brute force protection: PF
15 | * VPN: WireGuard, OpenIKED (optional), Unbound, PF
16 |
17 | ## Prerequisites
18 | If you want to enable *IPv6*, then add this line to your /etc/hostname.*:
19 | ```
20 | inet6 autoconf -temporary -soii
21 | ```
22 |
23 | You will have to set up some DNS records prior to running this script.
24 | Create the following DNS records:
25 | ```
26 | ;; Host TTL Type Value
27 | *.{domain}. 300 IN A {ip}
28 | {domain}. 300 IN A {ip}
29 | www.{domain}. 300 IN A {ip}
30 |
31 | ;; Only for IPv6:
32 | *.{domain}. 300 IN AAAA {ipv6}
33 | {domain}. 300 IN AAAA {ipv6}
34 | www.{domain}. 300 IN AAAA {ipv6}
35 | ```
36 |
37 | Use `ifconfig` to get your IP address or consult your VPS provider.
38 |
39 | **Note:** If you cannot use wildcard (*.{domain}.) record,
40 | set up these domains explicitly instead:
41 | `vpn.{domain}, mail.{domain}, www.vpn.{domain}, www.mail.{domain}, www.{domain}, {domain}`
42 |
43 | ## Usage
44 |
45 | 1. Get a VPS or a physical host with OpenBSD
46 | 2. Do the prerequisites (see above)
47 | 3. Create a user for yourself (**note: add user to group `wheel` and/or enable `doas` for your user**) and login
48 | 4. Create a dir for the scripts: `mkdir openbsd-server-setup && cd openbsd-server-setup`
49 | 5. Download the repo: `wget -O - https://github.com/d32f123/openbsd-server-setup/releases/download/latest/openbsd-server-setup.tar.gzip | tar -xzvf -`
50 | 6. `./setup.sh`
51 | 7. Follow the script's instructions
52 | 8. Do any post-install actions (see generated `post-install.txt`)
53 |
54 | ## Running
55 | ```sh
56 | ./setup.sh [bootstrap] [shell] [nginx] [ssl [--ssl-test]] [mail] [pf] [vpn]
57 | ```
58 | * When no options given, runs all stages sequentially
59 | * `--ssl-test` flag is used for [local development](./docs/development.md)
60 | * Before running the script, be sure to check the stages below and decide what you need.
61 | * All relevant post-install information will be available at `post-install.txt`
62 | after the script completes, so don't be afraid if you lose some of the script's output.
63 |
64 | ## Script stages
65 |
66 | **Stages and their package dependencies are located in [./scripts/](scripts/) directory**.
67 | Look for the `doas pkg_add ...` line in the beginning of the corresponding script.
68 |
69 | ### Stage 1 – [bootstrap]
70 |
71 | **Skip it if `doas` is already set up**
72 |
73 | Bootstrap does some basic configuration.
74 | Currently it enables main user to do `doas` and enables slaacd for IPv6.
75 |
76 | ### Stage 2 – [shell] setup
77 |
78 | Sets up an opinionated zsh+tmux environment.
79 | Completely optional.
80 |
81 | ### Stage 3 – [nginx] setup
82 |
83 | Depends on: **doas**
84 | Dependants: **ssl, mail, vpn**
85 |
86 | 1. Creates nginx configuration and logs directories
87 | 2. Creates configs and dirs for sites domain.xxx, mail.domain.xxx, vpn.domain.xxx.
88 | If you are not planning to use mail or vpn, you might want to remove some of these configs.
89 |
90 | Websites are located under /var/www/
91 | Configuration is located at /etc/nginx/
92 |
93 | ### Stage 4 – [ssl] setup
94 |
95 | Depends on: **doas, nginx**
96 | Dependants: **mail**
97 |
98 | 1. Gets certificates via certbot
99 | 2. Switches nginx configuration to use only secure versions of domains
100 |
101 | The certificates obtained here are also used to serve Mail frontend and VPN configurations.
102 |
103 | ### Stage 5 – [mail] server setup
104 |
105 | Depends on: **doas, nginx, ssl**
106 |
107 | 1. Sets up smtpd (main mail server), dovecot (IMAP server), rspamd (mail signing)
108 | 2. Creates a user account username@domainname
109 | 3. There are scripts available to add and delete users, change passwords
110 | 4. Makes local mail (sent by `$ mail ...` to local users) available over IMAP
111 | 5. Requires post-install procedures (see below)
112 | 6. (optional) sets up RainLoop web frontend. Available at mail.{{domain_name}}
113 |
114 | **Additional post-install**
115 |
116 | **Required**:
117 | This stage will spew out some additional DNS records, which confirm that
118 | mail is indeed coming from your domain name (spoofing protection).
119 |
120 | Optional: set up a reverseDNS record at your VPS provider
121 |
122 | **Note to VPS users**: port 25 is required to receive mail.
123 | If you're using VPS chances are it is blocked by default.
124 | You will have to contact your VPS provider to open port 25.
125 |
126 | ### Stage 6 – [pf] Packet Filter setup
127 |
128 | Sets up packet filter to block ips which spam your SSH, HTTP, HTTPS, IMAP, SMTP ports
129 |
130 | ### Stage 7 – [vpn] setup
131 |
132 | Depends on: **doas, nginx**
133 |
134 | Sets up WireGuard VPN and optionally OpenIKED IKEv2.
135 | Spins up a local Unbound DNS server for better privacy.
136 |
137 | VPN configurations for new clients can be created via a script (WireGuard only).
138 | Configurations are made available at a random endpoint at vpn.{{domain}}/
139 | QRs are provided to simplify importing configs to mobile clients.
140 |
141 | WireGuard uses asymmetric key + Preshared Key authentication. IKEv2 uses Preshared key authentication.
142 |
143 | ## Script parameters
144 | You can override the following envvars prior to running the script to modify it's behavior:
145 | - `USER_NAME` – the user which will be used for everything in the script. Defaults to current user.
146 | - `DOMAIN_NAME` – the domain name to create websites for. Defaults to `$(hostname | cut -d. -f2-)`
147 | - `MAIL_DOMAIN` – the domain name where mail server will be hosted. Defaults to `mail.$DOMAIN_NAME`
148 | - `VPN_DOMAIN` – the domain name where VPNs will be hosted (including their configurations). Defaults to `vpn.$DOMAIN_NAME`
149 |
150 | ## Feedback
151 |
152 | Feel free to provide feedback and imrpovemend ideas / report any issues here on GitHub (issues or pull requests)
153 | or mail me at . I will be grateful for any kind of feedback!
154 |
155 | ## Future ideas
156 | - Add OpenVPN support
157 | - Add a prompt to create a cron that rotates DKIM keys
158 | (will require manual rotation at the DNS provider side
159 | (may be possible to automate for certain providersvia API))
160 | - Consider migrating from nginx to built-in httpd
161 |
162 | ## [Development (see development.md)](./docs/development.md)
163 |
164 | ## [Acknowledgements (see acknowledgements.md)](./docs/acknowledgements.md)
--------------------------------------------------------------------------------
/scripts/005_mail.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ENVS="$(dirname $0)/../env.d"
4 | . "$ENVS/general.sh"
5 |
6 | echo "${YELLOW}Downloading dependencies${NORM}"
7 | doas pkg_add wget opensmtpd-extras opensmtpd-filter-senderscore opensmtpd-filter-rspamd \
8 | opendkim dovecot dovecot-pigeonhole rspamd || panic "Failed to download dependencies"
9 |
10 | . "$ENVS/mail.sh"
11 |
12 | doas [ ! -d "$CERT_DIR" ] && panic "Get an SSL certificate for $MAIL_DOMAIN first"
13 |
14 | echo "${YELLOW}Creating vmail account${NORM}"
15 | doas useradd -c "Virtual Mail Account" -d $VMAIL_ROOT -s /sbin/nologin -L staff $VMAIL_USER
16 | . "$(dirname $0)/../env.d/mail.sh"
17 |
18 | echo "${YELLOW}Configuring smtpd $MAIL_CONF${NORM}"
19 | doas cp -f $MAIL_CONF_DIR/{smtpd.conf,smtpd.bak.conf}
20 | sed "s/{{base_domain}}/$DOMAIN_NAME/g;
21 | s/{{mail_domain}}/$MAIL_DOMAIN/g;
22 | s/{{vmail_user}}/$VMAIL_USER/g;" mail/smtpd.template.conf | doas tee $MAIL_CONF >/dev/null
23 |
24 | echo "$MAIL_DOMAIN" | doas tee $MAIL_CONF_DIR/mailname >/dev/null
25 |
26 | for f in $CREDENTIALS $VIRTUALS $ALIASES; do
27 | doas touch $f
28 | doas chmod 0440 $f
29 | done
30 | doas newaliases
31 | doas chown _smtpd:_dovecot $CREDENTIALS $VIRTUALS $ALIASES $ALIASES.db
32 |
33 | doas mkdir $VMAIL_ROOT
34 | doas chown $VMAIL_USER:$VMAIL_USER $VMAIL_ROOT
35 |
36 | echo "${YELLOW}Making a specialized login group for dovecot${NORM}"
37 | echo "dovecot:\\
38 | :openfiles-cur=1024:\\
39 | :openfiles-max=2048:\\
40 | :tc=daemon:" | doas tee -a /etc/login.conf >/dev/null
41 | doas usermod -L dovecot _dovecot
42 | doas cap_mkdb /etc/login.conf # update login.conf db
43 |
44 | # Disable ssl file since we already put ssl info in local.conf
45 | doas mv /etc/dovecot/conf.d/10-ssl.conf /etc/dovecot/conf.d/10-ssl.conf.disabled
46 | doas rcctl restart dovecot || doas rcctl restart dovecot || doas rcctl restart dovecot || panic "Dovecot failed to start"
47 |
48 | echo "${YELLOW}Creating virtual user $USER_NAME${NORM}"
49 | doas rcctl enable smtpd
50 | doas rcctl restart smtpd
51 | mail/create_user.sh $USER_NAME || panic "Failed to create user $USER_NAME"
52 |
53 | main_user_mail_aliases="root abuse hostmaster postmaster webmaster"
54 | echo "${YELLOW}Adding aliases $main_user_mail_aliases for $USER_NAME${NORM}"
55 | for aliass in $main_user_mail_aliases; do
56 | echo "$aliass@$DOMAIN_NAME: $USER_NAME@$DOMAIN_NAME" | doas tee -a "$VIRTUALS" >/dev/null
57 | case "$aliass" in
58 | root|hostmaster|webmaster) echo "$aliass: $USER_NAME" | doas tee -a "$ALIASES" >/dev/null ;;
59 | esac
60 | done
61 | echo "$USER_NAME: $USER_NAME@$DOMAIN_NAME" | doas tee -a "$ALIASES" >/dev/null
62 | doas newaliases
63 |
64 | # TODO: Prompt to add additional virtual users
65 |
66 | echo "${YELLOW}Restarting smtpd service${NORM}"
67 | doas rcctl restart smtpd || panic "Failed to restart smtpd server"
68 |
69 | echo "${YELLOW}Configuring Dovecot${NORM}"
70 |
71 | SIEVE_ROOT=/usr/local/lib/dovecot/sieve
72 | spam_script=$SIEVE_ROOT/report-spam.sieve
73 | ham_script=$SIEVE_ROOT/report-ham.sieve
74 |
75 | sed "s/{{mail_domain}}/$MAIL_DOMAIN/g;
76 | s?{{credentials}}?$CREDENTIALS?g;
77 | s/{{vmail_uid}}/$VMAIL_UID/g;
78 | s/{{vmail_gid}}/$VMAIL_GID/g;
79 | s?{{vmail_root}}?$VMAIL_ROOT?g;
80 | s?{{spam_script}}?$spam_script?g;
81 | s?{{ham_script}}?$ham_script?g;" mail/dovecot.template.conf | doas tee /etc/dovecot/local.conf >/dev/null
82 |
83 | doas cp mail/report-ham.sieve mail/report-spam.sieve $SIEVE_ROOT
84 | doas chown root:bin $ham_script $spam_script
85 |
86 | echo "${YELLOW}Compiling spam and ham Sieve scripts${NORM}"
87 | doas sievec $ham_script
88 | doas sievec $spam_script
89 |
90 | doas cp mail/sa-learn-ham.sh mail/sa-learn-spam.sh $SIEVE_ROOT
91 | doas chown root:bin $SIEVE_ROOT/sa-learn-ham.sh $SIEVE_ROOT/sa-learn-spam.sh
92 |
93 | echo "${YELLOW}Restarting dovecot service${NORM}"
94 | doas rcctl enable dovecot
95 | doas rcctl restart dovecot || panic "Failed to load dovecot with the new configuration"
96 |
97 | echo "${YELLOW}Configuring spamd"
98 | doas mkdir $MAIL_CONF_DIR/dkim
99 | doas opendkim-genkey -D $MAIL_CONF_DIR/dkim -d "$DOMAIN_NAME" -s "$MAIL_DOMAIN"
100 | DKIM_SECRET_KEY="$MAIL_CONF_DIR/dkim/${MAIL_DOMAIN}.private"
101 | DKIM_PUBLIC_KEY="$MAIL_CONF_DIR/dkim/${MAIL_DOMAIN}.txt"
102 | doas chown root:_rspamd "$DKIM_SECRET_KEY"
103 | doas chmod 0440 "$DKIM_SECRET_KEY"
104 | doas chmod 0444 "$DKIM_PUBLIC_KEY"
105 |
106 | dns_dkim_record="$(<$DKIM_PUBLIC_KEY)"
107 | dns_spf_record="@ TXT v=spf1 mx a:$MAIL_DOMAIN -all"
108 | dns_dmarc_record="_dmarc.$DOMAIN_NAME TXT v=DMARC1; p=reject; rua=mailto:postmaster@$DOMAIN_NAME;"
109 | dns_records="$dns_dkim_record
110 | $dns_spf_record
111 | $dns_dmarc_record
112 | _imap._tcp.$DOMAIN_NAME. 300 IN SRV 0 0 0 .
113 | _imaps._tcp.$DOMAIN_NAME. 300 IN SRV 10 1 993 $MAIL_DOMAIN.
114 | _pop3._tcp.$DOMAIN_NAME. 300 IN SRV 0 0 0 .
115 | _pop3s._tcp.$DOMAIN_NAME. 300 IN SRV 0 0 0 .
116 | _submission._tcp.$DOMAIN_NAME. 300 IN SRV 20 1 587 $MAIL_DOMAIN.
117 | _submission._tcp.$DOMAIN_NAME. 300 IN SRV 30 1 25 $MAIL_DOMAIN.
118 | _submissions._tcp.$DOMAIN_NAME. 300 IN SRV 10 1 465 $MAIL_DOMAIN.
119 | "
120 | # TODO: Add a cronjob to rotate dkim keys every 3 months
121 |
122 | echo "$dns_records" >~/dns_records.txt
123 |
124 | doas mkdir -p /etc/rspamd/local.d
125 | sed "s/{{domain}}/$DOMAIN_NAME/g;
126 | s?{{dkim_private_key}}?$DKIM_SECRET_KEY?g;
127 | s/{{dkim_selector}}/$MAIL_DOMAIN/g;" mail/dkim_signing.template.conf | doas tee /etc/rspamd/local.d/dkim_signing.conf >/dev/null
128 |
129 | echo "${YELLOW}Restarting rspamd service${NORM}"
130 | doas rcctl enable redis rspamd
131 | doas rcctl start redis rspamd || panic "Failed to restart rspamd"
132 |
133 | prompt_bool "Install RainLoop WebMail?" "y" && {
134 | echo "${YELLOW}Installing RainLoop${NORM}"
135 | doas pkg_add php php-curl php-pdo_sqlite php-zip zip unzip
136 | cd "$(mktemp -d)"
137 | wget https://www.rainloop.net/repository/webmail/rainloop-latest.zip
138 | doas unzip -q rainloop-latest.zip -d /var/www/$MAIL_DOMAIN/
139 | cd -
140 | doas find /var/www/$MAIL_DOMAIN -type d -exec chmod 755 {} \;
141 | doas find /var/www/$MAIL_DOMAIN -type f -exec chmod 644 {} \;
142 | doas chown -R www:www /var/www/$MAIL_DOMAIN
143 |
144 | echo "${YELLOW}Modifying NGINX configuration for $MAIL_DOMAIN${NORM}"
145 | doas sed -i.pre_rainloop '
146 | s/index.html/index.php/;
147 | /include secure;/a\
148 | client_max_body_size 25M;\
149 | \
150 | location ^~ /data {\
151 | deny all;\
152 | }\
153 | \
154 | location ~ [^/]\.php(/|$) {\
155 | include fastcgi_params;\
156 | try_files $uri $uri/ =404;\
157 | fastcgi_split_path_info ^(.+?\.php)(/.*)$;\
158 | if (!-f $document_root$fastcgi_script_name) {\
159 | return 404;\
160 | }\
161 | fastcgi_param HTTP_PROXY "";\
162 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\
163 | fastcgi_index index.php;\
164 | fastcgi_pass unix:run/php-fpm.sock;\
165 | }\
166 | ' /etc/nginx/sites-available/$MAIL_DOMAIN.secure.site
167 | doas nginx -s reload || panic "Failed to reload nginx with PHP config"
168 |
169 | echo "${YELLOW}Configuring RainLoop${NORM}"
170 | PHP_VERSION=$(ls -d /etc/php-*.sample | head -1 | sed -E 's/.*php-(.*)\.sample/\1/')
171 | doas sed -i.bak -e 's/upload_max_filesize =.*$/upload_max_filesize = 25M/;
172 | s/post_max_size =.*$/post_max_size = 29M/;' /etc/php-$PHP_VERSION.ini
173 | doas cp /etc/php-$PHP_VERSION.sample/* /etc/php-$PHP_VERSION/.
174 |
175 | FPM_SERVICE=$(rcctl ls all | grep 'php.*_fpm' | head -1)
176 | doas rcctl enable $FPM_SERVICE
177 | doas rcctl start $FPM_SERVICE
178 | doas mkdir /var/www/etc
179 | for file in /etc/resolv.conf /etc/hosts
180 | do
181 | doas ln $file /var/www/etc || doas cp $file /var/www/etc
182 | done
183 |
184 | # This is to generate the RainLoop directories
185 | wget -O /dev/null --no-check-certificate $MAIL_DOMAIN
186 | RAINLOOP_ROOT=/var/www/$MAIL_DOMAIN/data/_data_/_default_
187 |
188 | doas sed -e "s/{{domain}}/$DOMAIN_NAME/;" mail/application.template.ini | doas tee $RAINLOOP_ROOT/configs/application.ini >/dev/null
189 | doas chown www:www $RAINLOOP_ROOT/configs/application.ini
190 |
191 | doas sed -e "s/{{mail_domain}}/$MAIL_DOMAIN/" mail/domain.template.ini | doas tee $RAINLOOP_ROOT/domains/$DOMAIN_NAME.ini >/dev/null
192 | doas chown www:www $RAINLOOP_ROOT/domains/$DOMAIN_NAME.ini
193 |
194 | echo "${PURPLE}${BOLD}RainLoop is available at: https://$MAIL_DOMAIN/" | postinstall
195 | }
196 |
197 |
198 | echo "${PURPLE}${BOLD}----MAIL CONFIGURATION DONE----"
199 | echo "${PURPLE}${BOLD}
200 | Use mail/{create,delete}_user.sh and mail/change_password.sh to manage accounts
201 | Now place these entries in your DNS records:
202 | $NORM$PURPLE$dns_records
203 | $NORM
204 | " | postinstall
--------------------------------------------------------------------------------