I self-host a lot of stuff these days. This blog is self-hosted for more than a decade, but I also run my own mail server since I think 2014, as well as various other small websites, a RSS aggregator, CalDAV synchronization, the Immich photo storage app, and whatnot.
The list is growing longer every year, and so is the attack surface. So I spent a bit of time to reduce the attack surface as far as it is easily possible.

Personal tools like RSS aggregator and CalDAV
These are only ever accessed by me (or my wife). Thus they require mTLS now, i.e the client needs to provide a TLS client certificate or the web server will refuse the connection.
The big advantage of mTLS is that no requests reach the hosted applications unless the client is already authenticated, so if they have e.g. bugs in their login mechanism, that doesn’t matter any more. It truly reduces attack surface. Brute-force attacks on credentials are also a solved problem.
mTLS setup: key data
Using mTLS is in principle simple but has a few stumbling blocks. In principle, on the server simply generate a new self-signed CA:
openssl ecparam -genkey -name secp256k1 -noout -out ca.key
openssl req -new -x509 -days 100000 -key ca.key -out ca.crt
On the client, generate a key and a CSR:
openssl ecparam -genkey -name secp256r1 -noout -out client-sven.key
openssl req -new -key client-sven.key -out client-sven.csr
Copy the CSR to the server and sign it, then copy the certificate back:
openssl x509 -req -in /tmp/client-sven.csr -CA ca.crt -CAkey ca.key -out /tmp/client-sven.cert -CAcreateserial -days 10000 -sha256
Pack the client’s key and certificate into a p12 archive:
openssl pkcs12 -export -macalg sha1 -legacy -out client-sven-legacy.p12 -inkey client-sven.key -in client-sven.cert
The legacy stuff here is needed for older android phones, which do not support SHA256 or whatnot and just say “wrong password”. The same goes for some choices of elliptic curve, if you import the p12 file you just get “wrong password”.
You can now copy this p12 archive to your andoid phone and import it into the cert store, and import it in your browser. Browsers will ask you for the certificate and you can select it when you browse to a protected site, and various android apps support selecting a certificate in their login config.
In my opinion for this “family” type use case, there is no reason to create multiple client certificates, it just complicates everything and provides basically no tangible benefits. If you want, it would require no changes to the setup though.
mTLS setup: httpd
Now, in your httpd.conf or so, you can simply point httpd to the CA file:
SSLCACertificateFile "/etc/httpd/ca/ca.crt"
SSLVerifyDepth 1
And now you can put
SSLVerifyClient require
into whatver <VirtualHost> directive you have to enable mTLS for that virtual host.
A small confusion: directory-specific mTLS
httpd does support SSLVerifyClient require for directories, either in its config or even in .htaccess files. However, in practice, this does not work. The reason is that doing this requires TLS renegotiation after the client has sent the path it wants to access as part of the initial session. That apparently used to be a thing ten years ago, but from what I have understood browsers have since dropped the feature because it was complicated to do for HTTP/2. Chrome doesn’t have it at all, and Firefox has it but it’s behind a config flag and I couldn’t get it to work, either.
So, unfortunately, that’s not a thing.
The more difficult case: mixed-access services like Immich
The photo management tool Immich now uses the same approach (except that that is in addition proxied to a box at my home because storage is expensive to rent in a data center and it needs a lot of RAM sometimes). Here, the problem is that you can create share links for albums and pictures, which are supposed to be public.
The solution here is to use a small proxy application which has much smaller attack surface to only serve the share links: https://github.com/alangrainger/immich-public-proxy This works nicely and isn’t hard to set up.

And what about WordPress?
WordPress (i.e. this blog) is another security disaster waiting to happen, so it would be good to reduce attack surface here a bit as well. My trust in WordPress not having undiscovered RCE bugs exploitable without auth is moderate at best. So I would feel better with some access control which goes beyond the usual eternally-repeated recommendations of keeping things up to date, using long passwords and checking file permissions.
To reduce attack surface, the most obvious thing I think I can do is limiting access to the admin interface and login form to known users (without having WordPress itself decide who is known and who isn’t). This is even recommended in many guides, but they usually limit by source IP which is not feasible for most ISPs. mTLS is not an option either, since WordPress needs public access and, as described above, per-directory mTLS is not really possible.
So I came up with the following contraption: I limit access to wp-login.php and the wp-admin folder to localhost via .htaccess:
Require ip 127.0.0.1
This makes the login form and admin interface, two big attack targets, only accessible from the server machine itself. The idea now is to use ssh to port-forward the server’s local access to the software to my notebook using ssh -L, and then edit posts on localhost.
Unfortunately it’s not quite that simple because 1) the server hosts multiple virtual hosts differentiated by ServerName which gets lost with this trick, and 2) WordPress uses absolute paths with the domain in it everywhere.
2) is easy to fix at least, in wp-config.php put
define('WP_HOME', '/');
define('WP_SITEURL', '/');
I don’t love it, but it’s an acceptable price to pay.
For 1), we need to get a little more crafty. We cannot differentiate by host name that the ssh-forwarded traffic should go to the blog.svenbrauch.de subdomain. But we can do it by port:
Listen 127.0.0.1:8081 http
<VirtualHost _default_:8081>
ServerName localhost
DocumentRoot /path/to/wordpress
... all the other stuff you might need
</VirtualHost>
With the new Listen directive, we make httpd open port 8081 as well. Since we only need it on localhost, no need to make it public. Then we create a copy of the VirtualHost entry for wordpress and catch-all redirect all traffic arriving on port 8081 there.
And now, you can simply
ssh -L 8081:localhost:8081 myserver
and then point your browser to http://localhost:8081/wp-login.php and log in normally and everything should work.
- Fourier Transform Spectroscopy continued, or: How to reproduce NIST ASD results to 8+ digits at home
Categories: Everything