A Glimpse into my Self-Hosting Setup
Edward Wibowo,
Self-hosting is one of my favorite pastimes.
It allows me to discover and maintain a wide range of services on my personal server, giving me the freedom and control I crave. From managing my own VPN and authentication service to hosting my own git instance, self-hosting empowers me to customize these services to my liking and have full control over my data.
But self-hosting is not a one-time setup — it’s an ongoing process. It involves continuous tweaks and changes to various configuration files. Over the time I have spent self-hosting, my setup has undergone significant transformations. It’s a journey that is both incredibly enjoyable and time-consuming.
In this post, my goal is to offer you a snapshot into my current self-hosting setup, some of the services I host, and how I secure my system.
Server Setup
My self-hosting setup revolves around a single Hetzner Virtual Private Server (VPS). I currently pay monthly for one of the cheaper plans (CPX 11) which costs me around €5 a month.
Here are the specs:
Component | Description |
---|---|
Distribution | Debian 12 (bookworm) |
vCPU | AMD EPYC (2) |
RAM | 2 GB |
Disk Space | 40 GB |
Traffic Bandwidth | 20 TB |
It’s a relatively modest setup. It won’t be serving out widely popular SaaS apps or running the latest LLMs anytime soon, but it works for my hobbyist purposes.
I used to maintain multiple physical servers — well, more like an old laptop from an e-waste bin and a Raspberry Pi 4 Model B. Hosting everything on my own hardware provided a greater sense of autonomy and control, but it’s more difficult to do so from a college dorm.
I do miss it though. There’s something incredibly satisfying about hosting stuff on your own hardware. I do hope to run my own self-hosting hardware in the future, perhaps even in a college dorm by leveraging WireGuard forwarding.
For now, the convenience of a VPS is hard to beat. The hardware is out of sight and out of mind. I really only have to worry about the software side of things.
Services
All the services I self-host are deployed with Docker and Docker Compose. Containerizing each of my services trivializes setting up, starting, and removing services to a single command. I also don’t have to concern myself with outdated or missing Debian packages when building third-party applications.
Here are the services I currently run:
➜ ~ ssh altaria
Linux altaria 6.1.0-10-amd64
➜ [claby2@altaria] ~ ls -1 docker/
authelia
blocky
cabbot
docs
dozzle
filebrowser
freshrss
gitea
themerepo
tools
voltproxy
Authelia
Some of the services I host on the web are exclusively for my personal use. To ensure that only authorized individuals (aka me) can access these services, I rely on Authelia as an authentication server.
Services without built-in authentication mechanisms are proxied with an authentication forwarding middleware (using voltproxy). This middleware directs clients to Authelia, which then redirects to the service if authenticaton is successful.
I defined a rule in my Authelia configuration to bypass authentication for devices on my WireGuard VPN network.
Authelia IP Bypass Configuration
Example authelia configuration.yaml
access_control:
default_policy: deny
networks:
- name: internal
networks:
- 10.1.2.0/24 # WireGuard subnet.
rules:
- domain: "*.example.com"
policy: bypass
networks:
- internal
- domain: "*.example.com"
policy: two_factor
This allows VPN-connected devies to access services without 2FA.
Blocky
Blocky is a DNS proxy and ad-blocker that I rely on for all of my WireGuard clients. It helps me proxy DNS queries and also blocks any annoying ads that might pop up.
cabbot
cabbot is a Discord bot I wrote in Rust using serenity-rs.
docs
docs (MkDocs) is my personal documentation. I use it to document technical issues, jot down random ideas, and keep a general journal of my thoughts. All of the notes are written in Markdown and beautifully rendered for the web. It’s my own private knowledge base.
Dozzle
Dozzle is a Docker log viewer accessible from the web.
Filebrowser
Filebrowser is a web-based file managing interface. I primarily use this to share files with other people and store documents.
FreshRSS
FreshRSS is an RSS feed aggregator and is one of my main sources of media consumption. Through my instance, I am able to subscribe to personal blogs, news outlets, software changelogs (via GitHub Atom feeds), and other link aggregators.
I have an app on my phone that syncs seamlessly with my FreshRSS instance. This means that whether I’m at home or on the go, I can always stay up to date with the latest updates and news.
When I’m on the terminal, I rely on Newsboat. It’s a fantastic terminal user-interface that I configured to sync effortlessly with my FreshRSS instance. This means that not only are the feeds synced, but also the read status and starred posts. It’s a seamless experience that keeps me organized and ensures that I never miss out on any important information. If you’re interested in learning more about how to set it up, you can find detailed documentation to guide you through the process.
Gitea
Gitea serves as my personal Git instance, providing the ideal platform for storing all my miscellaneous one-off repositories that will likely never be shared with the world.
Theme Repo
Theme Repo is a color scheme repository I made. I wrote a blog post about Theme Repo when I initially released it.
IT-Tools
IT-Tools is a website containing a collection of helpful developer tools.
voltproxy
And lastly but definitely not least (because I wrote it haha), voltproxy.
voltproxy is a reverse proxy that proxies all of the aforementioned services. Since my setup is primarily Docker-based, voltproxy makes it extremely easy to setup reverse proxying along with authentication forwarding and load balancing. Proxying a service is simply a matter of identifying the Docker container name, network, and port and specifying it all in a single YAML file.
Security
Next, I would like to go over some of the ways I secure my setup.
General SSH Security
One of the essential measures I implement is disabling SSH password login. By doing this, I limit SSH access to only those users who possess the appropriate public/private key pair. This effectively eliminates the vulnerability that arises from weak passwords.
Firewall Management
I rely on Hetzner’s in-built firewall manager to fortify my server’s network.
I can control which ports are open for publicly running services all from the web dashboard.
By doing so, I minimize the potential attack surface and reduce risk of unauthorized access.
I also generally try to avoid unnecessarily binding services to 0.0.0.0
.
HTTPS Enforcement
All of my publicly-accessible services are served with HTTPS. I rely on voltproxy’s automatic ACME-based certificate generation and renewal. It’s incredibly convenient because all I have to do is specify the domain in the configuration file, and voltproxy takes care of the rest.
Hardware Authentication
To add an extra layer of security, I use hardware authentication using YubiKey’s PIV module.
I currently own a YubiKey 4 (pictured above).
The key allows me to easily connect to my server from any machine in a secure manner. I have a separate SSH public key associated with the YubiKey to easily disable it in the case it gets lost, stolen, or damaged.
Here is a helper script I wrote to quickly provision keys:
ykssh.sh
#!/bin/sh
# Default path for the generated key
default_key_path="$HOME/public.pem"
# YubiKey slot to use for key generation
slot="9a"
error() {
echo "[ykssh ERROR]: $1" >&2
exit 1
}
prompt() {
while true; do
read -rp "$1" yn
case $yn in
[Yy]*) eval "$2" && break ;;
[Nn]*) break ;;
*) echo "Please answer yes or no." ;;
esac
done
}
command -v ykman >/dev/null || error "ykman is not installed"
prompt "Change PIN? [Y/n] " "ykman piv access change-pin"
prompt "Change PUK? [Y/n] " "ykman piv access change-puk"
prompt "Change Management Key? [Y/n] " "ykman piv access change-management-key"
echo "Testing pkcs11:"
pkcs11-tool --login --test || \
error "PKCS#11 not supported, have you installed OpenSC?"
read -rp "Choose key path [blank for default $default_key_path] " key_path
[ -z "$key_path" ] && key_path="$default_key_path"
echo "Generating key..." &&
ykman piv keys generate "$slot" "$key_path" &&
echo "Generaeting certificate..." &&
ykman piv certificates generate -s "CN=SSH-key" "$slot" "$key_path"
pkcs11_debian="/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so"
pkcs11_macos="/Library/OpenSC/lib/opensc-pkcs11.so"
pkcs11_other="/usr/lib/opensc-pkcs11.so"
# Find the appropriate PKCS#11 library and print the generated SSH public key
for pkcs11 in "$pkcs11_debian" "$pkcs11_macos" "$pkcs11_other"; do
[ -f "$pkcs11" ] && \
echo "Printing generated SSH public key" && \
ssh-keygen -D "$pkcs11" -e && exit 0
done
echo "Could not find PKCS#11 library"
exit 1
WireGuard Pre-shared Symmetric Keys
Although WireGuard is already considered to be highly secure, I take additional measures to enhance its security by implementing an extra layer of protection in the form of Pre-shared Symmetric Keys (PSKs). These PSKs serve as an additional barrier that adds an extra level of difficulty for anyone attempting to decrypt the traffic. This becomes particularly crucial in the event that a quantum computer manages to break Curve25519, which is the cryptographic algorithm used for key exchange in WireGuard. By incorporating PSKs, I ensure that even if key exchange were to be intercepted, the encrypted traffic remains safeguarded.
Regularly Updating Docker Images
To stay ahead of potential vulnerabilities, I make it a priority to regularly update my Docker container images. I considered using a service like watchtower to automatically update container images, but I prefer to read release notes and upgrade manually. I believe it’s important to be vigilant about what changes are being made in the software I use and serve.
I wrote a helper script to check if any running containers are using outdated image versions. It also attempts to suggest the latest version to use:
dockerqueryupdate.sh
This script queries the Docker socket, meaning it requires privileged access!
#!/bin/sh
# Iterate through all current containers and check if container image's version matches latest version.
digests() {
curl -s "https://hub.docker.com/v2/namespaces/$1/repositories/$2/tags/$3" | jq -r ".images[].digest"
}
suggestlatest() {
tags="$(curl -s "https://hub.docker.com/v2/repositories/$1/$2/tags?page_size=20" | jq -r -c ".results[]")"
echo "$tags" | while read -r tag; do
digest="$(echo "$tag" | jq -r ".images[].digest")"
name="$(echo "$tag" | jq -r ".name")"
[ "$name" != "latest" ] && [ "$digest" = "$3" ] && echo "$1/$2:$name" && break
done
}
images="$(curl -s --unix-socket /var/run/docker.sock http://localhost/containers/json | jq -r ".[].Image")"
echo "$images" | while read -r image; do
if echo "$image" | grep -q ":"; then
namespace="$(echo "$image" | grep -q "/" && echo "$image" | rev | cut -d"/" -f2 | rev || echo "library")"
repository="$(echo "$image" | cut -d":" -f1 | rev | cut -d"/" -f1 | rev)"
tag="$(echo "$image" | cut -d":" -f2)"
current="$(digests "$namespace" "$repository" "$tag")"
latest="$(digests "$namespace" "$repository" "latest")"
if [ "$current" != "$latest" ]; then
printf "%s -> %s\n" "$image" "$(suggestlatest "$namespace" "$repository" "$latest")"
fi
fi
done
Conclusion
Self-hosting is a great hobby time sink 💕.