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
permit nopass $USER as root
" > 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
https://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
-
Alpine is my preferred distribution for stable ‘appliance’ systems at the moment. Previously that has been OpenBSD.↩︎