├── .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 | QR Code 12 |
13 |
14 | wgclient.conf 15 |
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 --------------------------------------------------------------------------------