Lock Down Your Personal Website: Setting Up Client Certificate Authentication

Page content

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 openssl is 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 this
  • ca.pem: CA cert for Apache config
  • client-key.pem: client private key
  • client-cert.pem: client cert
  • client.csr: signing request
  • client.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 is
  • SSLVerifyClient require: cert is mandatory
  • SSLVerifyDepth 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

  1. Double-click the client.p12 file
  2. Keychain Access opens up
  3. Enter the cert password
  4. Goes into your login keychain
  5. Open Keychain Access
  6. Click “login” on the left
  7. Go to “My Certificates”
  8. Double-click “Default Company Ltd”
  9. Expand “Trust”
  10. Set “When using this certificate” to “Always Trust”
  11. Done

iPhone

  1. Get the client.p12 file over there somehow - AirDrop, iCloud, whatever works
  2. Open it in Files app
  3. See “Profile Downloaded” notification
  4. Go to Settings
  5. Tap Profile then Install
  6. Enter password
  7. 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.