Wanderer Server Configuration

This site runs on a server that I administer located in London. I call this server Wanderer, which is perhaps a bit of an oxymoron given that, being a lifeless hunk of metal, it does not move. I guess I’m the wanderer.

Key Specifications:
    architecture:   ARM/aarch64
    hostname:       wanderer
    cores:          4
    memory:         24gb
    network:        4gbps
    location:       London
    distribution:   Alpine Linux

The server itself should be considered ephemeral, able to be torn down and stood back up (relatively) trivially without any loss of state/data.

This is only slightly complicated by my current hosting provider not offering Alpine Linux1 images for its servers, but that can be worked around by performing an in place install of Alpine and nuking the default distribution.

The following three short sections contain all that is required to set up this server from scratch at my hosting provider. This configuration is made public in case it might be of use to anyone but will not work for your setup without reconfiguration.

Overwrite existing host operating system

Note: The following MUST be run from a cloud console, NOT via an SSH connection. If you don’t head this warning you WILL become locked out of your server.

From a cloud console, run the following commands in sequence to overwrite with Alpine Linux:

wget -O alpine.iso https://dl-cdn.alpinelinux.org/alpine/v3.17/releases/aarch64/alpine-standard-3.17.3-aarch64.iso
dd if=alpine.iso of=/dev/sda
reboot

Once Alpine is running from memory, login as root and execute the following:

mkdir /media/setup
cp -a /media/sda/* /media/setup
mkdir /lib/setup
cp -a /.modloop/* /lib/setup
/etc/init.d/modloop stop
umount /dev/sda
mv /media/setup/* /media/sda/
mv /lib/setup/* /.modloop/

(Optional) Write the following file with vi answers:

KEYMAPOPTS="us us"          # Use US layout with US variant
HOSTNAMEOPTS="wanderer"       # Set hostname to 'alpine'
DEVDOPTS=mdev               # Set device manager to mdev
TIMEZONEOPTS="UTC"          # Set timezone to UTC
SSHDOPTS="-c openssh"          # SSH server
DISKOPTS="-m sys /dev/sda"  # Use /dev/sda as a sys disk
NTPOPTS="-c chrony"         # Use openntpd
PROXYOPTS="none"            # proxy options

# Create admin user
USEROPTS="-a -u -g audio,video,netdev silas"
USERSSHKEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDXX3YD768Qg1qPwCQ09baRT/XfuChhs73tociGyY2ba52FWuwiZSMt+6hDkcZzfweLUkbApt3T4OCz8SsWcouckBiwRr9IBnsUdhqfQI/hzGvnS7WTNaL1ytqIM5zqrKLG0gzMzbizvR7/ekki/Crqn8pREqEkQp0T+h7nqSj5hEXv+6Hgoc2Ca60/W2o871IE0+9wHW4qmGGkvk63R4dcIBxbl6WTArgEsbHkKRm8Tdj+g4UnapkRo9ArZf4+dnBGJUh4sVTx62b93HW5MNGUSY73mfi+oyeikeaOU1wOTVp2YD+8lZHAxiSEnwBp536hwgpdgk+PZb3igx5ovapFmcGUdSYYAQ7/65xzQsChNXXv88M6jEGksYmiovDqR/nMBsY9VvxK3OPiyyhEzo9n3rofUn6nUr3t80f+2mKr1rZaOOCSBjDp6hHHD5N90tsNxOugpn5DWVPyyoX5PqZgA+Ya0bepoe7kmdOqf5Nn76OfQr3OVstEG4DMPHpsVwEkiRid/V+xSoR+vcqPWMk4MltZUeEnMli0EVYXEpnCdW7D/rjmY1UUc82i/eJc36iPCtIoxBosTkLefe6HMo3x8dG/DyYgoLasFSRw4jfR1W1RNKalMH/x3Xng0xN/WWbXerPbwL5iGmg7uZf0VE608H5Rnpp65FdcprB1n2DqpQ=="

# Contents of /etc/network/interfaces
INTERFACESOPTS="auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp
    hostname alpine-test
"

Then install interactively with setup-alpine:

setup-alpine -f answers

If not using the above answer file, just run setup-alpine.

Either way, once install is complete, reboot the system.

Post install script

The post install script takes care of setting doas/privileges, disables the default message of the day, enables repositories, installs packages, and sets up a secure firewall.

Write the following script to a file eg, vi postinstall, make it executable (chmod +x postinstall), and run it (./postinstall):

#!/bin/sh -e

if test ! -f /etc/apk/world; then
  echo "Pretty sure this aint an alpine system fella."
  exit 111
fi

# Replace the doas config, permitting passwordless doas for the current $USER
printf "permit persist :wheel\npermit nopass $USER as root\n\n" > doas.conf
doas chown root:wheel doas.conf
doas mv doas.conf /etc/doas.d/doas.conf

# Disable the 'message of the day'
doas rm /etc/motd

# Enable 'latest-stable' main and community repos
printf "https://uk.alpinelinux.org/alpine/latest-stable/main\nhttps://uk.alpinelinux.org/alpine/latest-stable/community" > repositories 
doas mv repositories /etc/apk/repositories
doas apk update
doas apk upgrade

# Install desired packages
doas apk add rsync curl iptables ip6tables awall goaccess openssl acme-client lsblk

# Setup firewall
doas mkdir -p /etc/awall/optional
mkdir -p awall
printf '{"description": "Basic awall policy: sets up variables and rejects all traffic", "variable": { "internet_if": "eth0" }, "zone": { "internet": { "iface": "$internet_if" } }, "policy": [{ "in": "internet", "action": "drop" }, { "action": "reject" }] }' > awall/base.json
printf '{"description": "Allow outgoing connections for dns, http/https, ssh, ntp, ssh and ping", "filter": [ { "in": "_fw", "out": "internet", "service": [ "dns", "http", "https", "ssh", "ntp", "ping" ], "action": "accept" } ] }' > awall/outgoing.json
printf '{ "description": "Allow incoming ping", "filter": [ { "in": "internet", "service": "ping", "action": "accept", "flow-limit": { "count": 10, "interval": 6 } } ] }' > awall/ping.json
printf '{ "description": "Allow incoming SSH access (TCP/22)", "filter": [ { "in": "internet", "out": "_fw", "service": "ssh", "action": "accept", "conn-limit": { "count": 5, "interval": 10 } } ] }' > awall/ssh.json
printf '{ "description": "Allow incoming HTTP/HTTPS (TCP/80 and 443) ports", "filter": [ { "in": "internet", "out": "_fw", "service": [ "http", "https"], "action": "accept" } ] }' > awall/webserver.json
doas mv awall/*.json /etc/awall/optional/
doas awall enable base ping outgoing ssh webserver 
doas awall activate

Webserver config

I’m currently using Caddy web server to host this website. Almost any webserver would suffice (nginx, Apache, lighttpd, etc) but I like the ergonomics of Caddy’s configuration.

doas apk add caddy caddy-openrc
doas mkdir -p /var/log/caddy
doas chown -R caddy:caddy /var/log/caddy

Caddyfile (/etc/caddy/Caddyfile)

silasjelley.com {
        root * /home/silas/silasjelley.com
        encode gzip zstd
        file_server
        log {
                output file /var/log/caddy/silasjelley.com.log {
                        roll_disabled
                }
        }
        try_files {path} {path}/index.html {path}/index.xml

        # Security
        header {
                # Enable HTTP Strict Transport Security (HSTS)
                Strict-Transport-Security max-age=31536000;
                # Enable cross-site filter (XSS) and tell browser to block detected attacks
                X-XSS-Protection "1; mode=block"
                # Strict Content Security Policy
                Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inlin
                # Block FLoC
                Permissions-Policy interest-cohort=()
                # Disallow the site to be rendered within a frame (clickjacking protection)
                X-Frame-Options "Deny"
                # Stop clients from sniffing media type
                X-Content-Type-Options nosniff
                # Allow embedding GPX Studio maps
                Access-Control-Allow-Origin: https://gpx.studio
        }

        # Redirects & Rewrites
        rewrite /publickey /publickey.txt
        rewrite /sitemap /sitemap.xml
        redir /journeys /wandering permanent
        redir /marmedala /before permanent 

        handle_errors {
                @404-410 expression `{err.status_code} in [404, 410]`
                handle @404-410 {
                        rewrite * /404
                }
                handle {
                        respond "That's an error"
                }
        }
}

Start the web server and add it the default runlevel (startup at boot)

doas rc-service caddy start
doas rc-update add caddy default

Caddy’s config can then be indempotently reloaded with doas rc-service caddy reload.

Alternate webserver (nginx)

Write an NGINX config to /etc/nginx/nginx.conf:

user                            silas;
worker_processes                auto;
worker_rlimit_nofile            100000; # number of file descriptors used for nginx
error_log                       /var/log/nginx/error.log warn;

events {
    worker_connections          4096;
    use epoll;
}

http {
    include                     /etc/nginx/mime.types;
    default_type                application/octet-stream;
    sendfile                    on; 
    tcp_nopush                  on;

    gzip                        on;
    gzip_vary                   on;
    gzip_min_length             10240;
    gzip_comp_level             1;
    gzip_disable                msie6;
    gzip_proxied                expired no-cache no-store private auth;
    gzip_types
        text/css
        text/javascript
        text/xml
        text/plain
        application/javascript
        application/x-javascript
        application/json
        application/xml
        application/rss+xml
        application/atom+xml
        font/truetype
        font/opentype
        image/svg+xml;


    access_log                  /var/log/nginx/access.log;
    keepalive_timeout           3000;

    #ssl_protocols                   TLSv1.1 TLSv1.2 TLSv1.3;
    #ssl_prefer_server_ciphers       on;
    #ssl_session_cache               shared:SSL:2m;
    #ssl_session_timeout             1h;
    #ssl_session_tickets             off; # Disable TLS session tickets (they are insec

    server {
        listen                  80;
        root                    /home/silas/silasjelley.com;
        index                   index.html;
        server_name             localhost;
        client_max_body_size    32m;
        error_page              500 502 503 504  /50x.html;
        rewrite                 /publickey /publickey.txt;
        rewrite                 /sitemap /sitemap.xml;
        rewrite                 ^(/feeds/.*) $1/index.xml last;
        location = /50x.html {
              root              /var/lib/nginx/html;
        }
        location ~* .(jpg|jpeg|png|gif|ico|css|js)$ {
            expires 30d;
        }
    }
}

Start the NGINX server and make sure it starts at every boot:

doas rc-service nginx start
doas rc-update add nginx default


  1. Alpine is my preferred distribution for stable ‘appliance’ systems at the moment. Previously that has been OpenBSD.↩︎︎