Lock Down Your Personal Website: Setting Up Client Certificate Authentication
So you want a website nobody else can touch.
If you’re self-hosting anything with sensitive data, you probably need this.
This is how I set up client certificate auth on Apache so only my devices can even connect.
Why I Did This
Setting up web apps nowadays is dead simple.
Tons of open source stuff, free hosting everywhere, or just roll your own server.
But hackers have also gotten way better.
Running a personal site on the open internet with private data?
Username and password alone doesn’t cut it anymore IMO.
You could do IP-based ACLs I guess.
Secure as hell, but then you loose the whole point of a web app.
Can’t access it from anywhere.
Client certificates fix this.
Works kinda like an ACL, but instead of IPs, only browsers with the cert installed can connect.
Everyone else gets nothing.
This is perfect for personal sites only you use.
I’m writing this down so I can remeber how I did it next time.
What We’re Building
- First layer: Client SSL cert check at the Apache level
- Second layer: Normal login with username/password
- End result: No cert means you can’t even see the login page
This is basically what banks do.
Attacker needs both your cert file AND your password.
Good luck with that.
My Setup
- OS: Rocky Linux 9.7
- Web server: Apache 2.4
- Domain: sample-website.com (just an example)
- Clients: macOS Safari, Chrome, iPhone Safari
Creating the Certificates
Working Directory
sudo mkdir -p /etc/httpd/client-certs
cd /etc/httpd/client-certs
CA Private Key
sudo openssl genrsa -aes256 -out ca-key.pem 4096
It’ll ask for a password.
Pick something you won’t forget.
FYI
opensslis the tool that handles all the certificate stuff.
CA Certificate
sudo openssl req -new -x509 -days 3650 -key ca-key.pem -sha256 -out ca.pem
Yeah I set it for 10 years.
I know I can keep track of these files so whatever.
When it asks for details just hit Enter through everything.
What the flags do:
-days 3650: 10 years-x509: self-signed format-sha256: SHA-256 encryption
Client Private Key
sudo openssl genrsa -out client-key.pem 4096
No password this time.
Just Enter.
Certificate Signing Request
sudo openssl req -new -key client-key.pem -out client.csr
Enter through all the prompts again.
Sign It With the CA
sudo openssl x509 -req -days 3650 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -sha256
Use the CA password from earlier.
10 years again.
Don’t lose this for the next decade.
Browser-Ready PKCS12 File
sudo openssl pkcs12 -export -out client.p12 -inkey client-key.pem -in client-cert.pem -certfile ca.pem
Set a password here.
This is what you type when installing the cert in your browser.
What We Got
Here’s the files now:
ca-key.pem: CA private key - don’t leak thisca.pem: CA cert for Apache configclient-key.pem: client private keyclient-cert.pem: client certclient.csr: signing requestclient.p12: this goes in your browser
Apache Config
SSL Config Changes
Tell Apache to require client certs for HTTPS.
sudo vi /etc/httpd/conf.d/sample-website.com-ssl.conf
Add these 3 lines after SSLCertificateKeyFile:
SSLCACertificateFile /etc/httpd/client-certs/ca.pem
SSLVerifyClient require
SSLVerifyDepth 2
What these mean:
SSLCACertificateFile: where the CA cert isSSLVerifyClient require: cert is mandatorySSLVerifyDepth 2: cert chain depth
Full example (yours will be diffrent):
<VirtualHost *:443>
ServerName sample-website.com
ServerAlias www.sample-website.com
DocumentRoot /var/www/vhosts/sample-website.com/public_html
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/sample-website.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/sample-website.com/privkey.pem
# Client cert stuff
SSLCACertificateFile /etc/httpd/client-certs/ca.pem
SSLVerifyClient require
SSLVerifyDepth 2
<Directory "/var/www/vhosts/sample-website.com/public_html">
AllowOverride All
Require all granted
</Directory>
ErrorLog /var/log/httpd/sample-website.com_ssl_error.log
CustomLog /var/log/httpd/sample-website.com_ssl_access.log combined
</VirtualHost>
Test and Restart
sudo apachectl configtest
sudo systemctl restart httpd
sudo systemctl status httpd --no-pager
Should say Syntax OK and active (running).
Installing the Certificate
Get the p12 File
Download client.p12 to your computer however you want.
Might need to fix permissions first:
sudo chmod 644 /etc/httpd/client-certs/client.p12
Rename it to something useful.
Future you will have no idea what “client.p12” is.
macOS
- Double-click the
client.p12file - Keychain Access opens up
- Enter the cert password
- Goes into your login keychain
- Open Keychain Access
- Click “login” on the left
- Go to “My Certificates”
- Double-click “Default Company Ltd”
- Expand “Trust”
- Set “When using this certificate” to “Always Trust”
- Done
iPhone
- Get the
client.p12file over there somehow - AirDrop, iCloud, whatever works - Open it in Files app
- See “Profile Downloaded” notification
- Go to Settings
- Tap Profile then Install
- Enter password
- Done
Testing
Time to see if it actually works.
With the Certificate
Open your browser:
https://sample-website.com
- Cert selection popup shows up
- Pick “Default Company Ltd”
- Click Allow or Continue
- Site loads
Without the Certificate
Try from another machine:
https://sample-website.com
- Says it needs a certificate
- Nothing to select
- Nothing loads
CURL Test
From a machine without the cert:
curl -v https://sample-website.com
You’ll see:
SSL handshake failed
SEC_E_ILLEGAL_MESSAGE
Dies right at the handshake.
Can’t even get to the login screen.
End Result
What We Got
Layer 1: Client Cert at SSL Level
- No cert no connection
- Can’t see anything not even the login page
- 10 year validity because it’s my damn site
Layer 2: Username/Password
- Still need to login even with the cert
- Actual two factor setup
About HTTP
I don’t serve anything on port 80 but I need it for Let’s Encrypt renewals.
Here’s what I use:
<VirtualHost *:80>
ServerName sample-website.com
# Let's Encrypt needs this
Alias /.well-known/acme-challenge/ /var/www/html/.well-known/acme-challenge/
<Directory "/var/www/html/.well-known/acme-challenge/">
Require all granted
</Directory>
# Everything else goes to HTTPS
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>
Let’s Encrypt works fine.
Everything else hits HTTPS and dies without the cert.
Thats It
Keep your certificate safe.
Install it whenever you get a new device.
Lost it?
Just make a new one.
Run through the steps again.
Attackers get blocked at the connection level.
They don’t even get to try a password.
See? Easy.