This article explains how to securely access your home server and its services with mTLS and WireGuard.
It is part of my series on creating a home server on an old laptop. I’m assuming that you’ve set up Docker, Caddy, and Authelia, as described in the first article of the series.
While the described techniques work for all services, I focus on Nextcloud and Immich, which are part of the series.
Is Exposing Your Services to the Internet Secure? #
When you expose your apps and services from your home server to the public Internet, you are immediately a target for exploits, malware, and so on and so forth. Hence, it is generally not a good idea. If you can, avoid it.
That is why the common strategy of self-hosters is to choose a VPN like WireGuard or OpenVPN to access their services. With a VPN, your accessing device becomes part of your local network and your services don’t need to be exposed.
I Did It Anyway #
I had the requirement to make Immich and Nextcloud available for non-tech save people like my parents and parents in law. Teaching them that my services can only be accessed with a VPN seemed impossible. Also, I find that WireGuard on Android drains the battery unnecessarily heavy.
Hence, I started searching for alternatives and found Mutual TLS.
Mutual TLS #
With Mutual TLS (mTLS), also known as client certificate authentication, both client and server have a certificate, and both sides authenticate using their public/private key pair. That is different to normal TLS, where only the server has a certificate.
Benefits #
This yields to the following benefits when used with a reverse proxy like Caddy:
- Attackers need a valid private key to authenticate to the reverse proxy. The private key is secured with a passphrase, too.
- The mTLS handshake happens before access to the web service is granted. Hence, attackers can’t use exploits or issues in web services as they can’t see the web service in the first place.
- mTLS is transparent to the user. You install a certificate on the user’s devices once and, they can transparently use your services around the globe.
- No additional battery consumption.
Downsides And Constraints #
If your ISP is putting you behind CGNAT, mTLS does not work. You need a VPN then.
With mTLS you expose your public IP. If that is a problem for you, a VPN is the only option. It’s not a problem for me, though.
A more serious constraint is, that mTLS is not well-known and that not all services or devices support it. Luckily, Immich and Nextcloud both support mTLS.
Also, you have to install a certificate on every device. For me, that means I have to install certificates on multiple laptops, Android, and iOS devices of my parents, wife, and so on. Unfortunately, Android Family Link does not support installing certificates. So it’s really a manual step. Depending on the certificate validity, this task becomes tedious. Still the pros outweighed the cons for me.
WireGuard for Everything Else #
I’m the sole user of the other services that are running on my home server and, I’m fine accessing them via WireGuard. My router, a Fritz Box, can act as a WireGuard server out-of-the-box and that’s what I’m using.
But, as soon as I get to know that mTLS is supported for one of these services, I’ll make the switch as it is much more convenient.
Network Architecture #
This section shows the final network architecture for my home lab.
Internal Traffic #
Services that are not exposed to the Internet can be accessed through the normal Caddy container that runs on port 443. That is also true for devices that are connected via WireGuard.
External Traffic Via Second Caddy Reverse Proxy #
I added a second Caddy reverse proxy (caddy-pub) just for external services. This allows me to separate settings for external and internal services more easily. It also saves me from accidentally publishing services through my normal Caddy reverse proxy.
For the external services, I’ve set caddy-pub to listen on port 444 and configured forwarding from the public port 443 on my router to caddy-pub on port 444.
Firewall Changes #
Allow port 444 through your firewall on your home server. If you’ve followed my series, add the following to the home.yml
variable file.
ufw_rules:
- rule: allow
to_port: "444"
protocol: tcp
DNS #
To access your services from the Internet you need public DNS entries pointing to your public IP. But, the public IP assigned by your ISP changes. You need a dynamic DNS service to solve that problem. My Fritz Box router has this option built-in. If you don’t have a Fritz Box, IPv64.net is a service I can recommend.
For every service you want to expose, add a CNAME entry pointing to the dynamic DNS address.
CNAME nextcloud.home.dominikbritz.de -> Your-DynDNS-String.myfritz.net
caddy-pub Docker Container #
Docker Preparations With Ansible #
As described in my Immich and Nextcloud articles, you would rather not run the caddy-pub container as root as it is exposed to the Internet.
Create an Ansible role for caddy-pub to run with limited permissions like this.
Compose File #
Create the Caddy container like this. Below is my Docker compose file for reference.
services:
caddy-pub:
build: ./dockerfile-dns
container_name: caddy-pub
hostname: caddy-pub
restart: unless-stopped
ports:
- 444:443
- 444:443/udp
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./data:/data
- ./config:/config
- /usr/local/data/certificates:/certificates:ro # Path to the CA and client certificates root folder
networks:
- caddy_caddynet
networks:
caddy_caddynet:
external: true
Create a Certificate Authority #
You need to create a certificate authority (CA) on your home server. With the CA you can then issue certificates for your devices. On your devices, you install the device certificate as well as the CA certificate.
The following commands are optimized to provide all required input at the command line. There is no interactive input that annoys you. There are no config files you have to mess around with. All necessary steps are executed by a single OpenSSL invocation: from private key generation up to the self-signed certificate.
The first step is to install certtool which we require later for Android certificates. Use the command below or, better yet, add it to your base Ansible playbook.
apt install gnutls-bin
Second, create some folders that hold your CA and client certificates. Make sure that it is the same path as in the compose file.
mkdir -p /usr/local/data/certificates/ca
mkdir -p /usr/local/data/certificates/clients
Third, create the CA. Change the domain name accordingly.
cd /usr/local/data/certificates/ca
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -keyout home.dominikbritz.de.key -out home.dominikbritz.de.crt -subj "/CN=home.dominikbritz.de" \
-addext "subjectAltName=DNS:home.dominikbritz.de,DNS:*.home.dominikbritz.de"
Each of the above commands creates a certificate that is
- valid for the domain
home.dominikbritz.de
, - also valid for the wildcard domain
*.home.dominikbritz.de
, - relatively strong but also highly compatible by using RSA with 4096 bits
- valid for 3650 days (~10 years).
The following files are generated:
- Private key:
home.dominikbritz.de.key
- Certificate:
home.dominikbritz.de.crt
Create the Client Certificates #
For each of your users or each of your devices, depending on how complex you want to have it, run the following commands.
# Nice oneliner that only prompts for a passphrase
# Replace client1 and the CA accordingly
# -days lets you specify the certificate validity
cd /usr/local/data/certificates/clients
openssl req -new -newkey rsa:4096 -keyout client1.key -subj "/C=DE/ST=State/L=Location/O=Organisation/CN=client1" | openssl x509 -req -CA ../ca/home.dominikbritz.de.crt -CAkey ../ca/home.dominikbritz.de.key -CAcreateserial -out client1.crt -days 3650
Android requires different hashes and ciphers. Run the below.
cd /usr/local/data/certificates/clients
certtool --load-privkey client1.key --load-certificate client1.crt \
--load-ca-certificate ../ca/home.dominikbritz.de.crt \
--to-p12 --outder --outfile client1-android.p12 \
--p12-name "client1-android" \
--hash SHA1 --pkcs-cipher 3des-pkcs12 --password YourPassword
Caddy Configuration #
Implementing mTLS with Caddy is a breeze. Add the following reusable block at the top of your Caddyfile
(details).
(mTLS) {
client_auth {
mode require_and_verify
trusted_ca_cert_file /certificates/ca/home.dominikbritz.de.crt
}
}
Then import it for your services in the tls
statement. Here is an example for Nextcloud (details).
nextcloud.{$MY_DOMAIN} {
reverse_proxy nextcloud:80
redir /.well-known/carddav /remote.php/dav 301
redir /.well-known/caldav /remote.php/dav 301
redir /.well-known/webfinger /index.php/.well-known/webfinger
redir /.well-known/nodeinfo /index.php/.well-known/nodeinfo
tls {
import mTLS
dns netcup {
customer_number {env.NETCUP_CUSTOMER_NUMBER}
api_key {env.NETCUP_API_KEY}
api_password {env.NETCUP_API_PASSWORD}
}
propagation_timeout 900s
propagation_delay 600s
resolvers 1.1.1.1
}
import personal_headers
}
Instruct caddy-pub to reload its configuration by running:
docker exec -w /etc/caddy caddy-pub caddy reload
Backup the Certificates #
Add the certificates to your backup. If you’ve followed my series, add the following to data/resticprofile/profiles.yml
(details).
source:
- "/usr/local/data/certificates"
Client Certificate Installation #
Android #
Install the CA and client certificate in the Android system store.
- CA certificate: Settings app -> Security & privacy -> More security settings -> Encryption & credentials -> Install certificate -> CA certificate
- Client certificate: Settings app -> Security & privacy -> More security settings -> Encryption & credentials -> Install certificate -> VPN & app user certificate
Nextcloud #
The Nextcloud Android app uses the certificate in the system store. There is nothing to configure.
Immich #
- If you are already logged on, log off first.
- Go to Settings -> Advanced -> SSL Client Certificate -> Import and import the
client1-android.p12
certificate. - Log on again.
Firefox/Chrome #
While there are articles on the Internet claiming that they can use Firefox on Android with mTLS, I was unable to do so. Chrome works fine with certificates in the system store, though.
So if you have apps or services that you access through the browser only secured with mTLS, Chrome is your best bet on Android.
Windows #
Use PowerShell to import the certificates.
# Import the server CA
Import-Certificate -FilePath "home.dominikbritz.de.crt" -CertStoreLocation Cert:\LocalMachine\Root
# Import the client TLS certificate and key
Import-PfxCertificate -FilePath "client1.p12" -CertStoreLocation Cert:\LocalMachine\My