RunBSD

Client Certificate Authentication on OpenBSD

6 June 2026

Goal: set up a private section of runbsd.me that is not reachable by the public. No login page, no passwords. Instead, use TLS client certificate authentication — the connection is refused at the TLS handshake level if you don't have the right certificate. Nothing to brute force, nothing to phish.

The private section lives at https://runbsd.me:8443/. Port 8443 gets its own server block in httpd.conf with client ca set. The public site on port 443 is untouched.

The CA lives on my local zimaboard (OpenBSD 7.9-current, not internet-facing), behind OPNsense. The CA private key never touches the Amsterdam VM. Only the CA public certificate gets copied there.

On the zimaboard — set up the CA

Create the directory structure

mkdir -p ~/ca/{private,certs,csr,newcerts}
chmod 700 ~/ca/private
touch ~/ca/index.txt
echo 1000 > ~/ca/serial

Create ca.cnf

Save this to ~/ca/ca.cnf. The important bits are CA:true in the v3_ca section and clientAuth in v3_client.

[ ca ]
default_ca = CA_default

[ CA_default ]
dir               = /home/dhw/ca
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/newcerts
database          = $dir/index.txt
serial            = $dir/serial
private_key       = $dir/private/ca.key
certificate       = $dir/certs/ca.crt
default_days      = 375
default_md        = sha256
preserve          = no
policy            = policy_strict

[ policy_strict ]
countryName             = optional
stateOrProvinceName     = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ req ]
default_bits        = 4096
distinguished_name  = req_distinguished_name
string_mask         = utf8only
default_md          = sha256
x509_extensions     = v3_ca

[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
organizationName                = Organization Name
organizationalUnitName          = Organizational Unit Name
commonName                      = Common Name
emailAddress                    = Email Address

[ v3_ca ]
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always,issuer
basicConstraints        = critical, CA:true
keyUsage                = critical, keyCertSign, cRLSign

[ v3_client ]
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always,issuer
basicConstraints        = critical, CA:false
keyUsage                = critical, digitalSignature
extendedKeyUsage        = clientAuth

Generate the CA private key

4096-bit RSA, encrypted with AES-256. You'll set a passphrase here — keep it somewhere safe, you need it every time you sign a certificate.

openssl genrsa -aes256 -out ~/ca/private/ca.key 4096
chmod 400 ~/ca/private/ca.key

Generate the CA certificate

Self-signed, valid for 10 years.

openssl req -new -x509 -days 3650 \
    -config ~/ca/ca.cnf \
    -extensions v3_ca \
    -key ~/ca/private/ca.key \
    -out ~/ca/certs/ca.crt

Verify the CA certificate

Check for CA:TRUE, Certificate Sign, and 10-year validity.

openssl x509 -in ~/ca/certs/ca.crt -text -noout

On the zimaboard — issue a client certificate

Generate the client key

Elliptic curve (P-256) rather than RSA — smaller and faster, perfectly suited for client certs. Do not cat the key file after generating it.

openssl ecparam -name prime256v1 -genkey -noout \
    -out ~/ca/private/dhw-client.key
chmod 400 ~/ca/private/dhw-client.key

Create a certificate signing request

openssl req -new \
    -key ~/ca/private/dhw-client.key \
    -out ~/ca/csr/dhw-client.csr \
    -subj "/C=US/ST=California/L=San Clemente/O=DHW Home CA/CN=DHW Personal Client"

Sign the CSR with your CA

Valid for 2 years. You'll be prompted for your CA passphrase.

openssl x509 -req \
    -days 730 \
    -in ~/ca/csr/dhw-client.csr \
    -CA ~/ca/certs/ca.crt \
    -CAkey ~/ca/private/ca.key \
    -CAcreateserial \
    -out ~/ca/certs/dhw-client.crt \
    -extensions v3_client \
    -extfile ~/ca/ca.cnf

Verify the client certificate

Check for CA:FALSE, TLS Web Client Authentication, and Digital Signature.

openssl x509 -in ~/ca/certs/dhw-client.crt -noout -dates -subject -issuer

Bundle into a .p12 for browser import

You'll be prompted for your CA passphrase, then asked to set an export password for the bundle. The export password just needs to survive long enough to import into the browser.

openssl pkcs12 -export \
    -out ~/ca/dhw-client.p12 \
    -inkey ~/ca/private/dhw-client.key \
    -in ~/ca/certs/dhw-client.crt \
    -certfile ~/ca/certs/ca.crt \
    -name "DHW Personal Client"
chmod 400 ~/ca/dhw-client.p12

Copy files to Mac and Amsterdam VM

Copy the .p12 and CA cert to the Mac

# run these on the Mac
scp dhw@ZIMABOARD_IP:~/ca/dhw-client.p12 ~/Desktop/dhw-client.p12
scp dhw@ZIMABOARD_IP:~/ca/certs/ca.crt ~/Desktop/dhw-ca.crt

Copy the CA public cert to the Amsterdam VM

# run on the Mac
scp ~/Desktop/dhw-ca.crt dhw@runbsd.me:/tmp/dhw-ca.crt

On the Amsterdam VM — configure httpd

Move the CA cert into place

doas mv /tmp/dhw-ca.crt /etc/ssl/dhw-ca.crt
doas chown root:wheel /etc/ssl/dhw-ca.crt
doas chmod 644 /etc/ssl/dhw-ca.crt

Create the private document root

doas mkdir -p /var/www/htdocs/runbsd.me/private

Add a server block to /etc/httpd.conf

This reuses the existing Let's Encrypt certificate. The client ca directive inside the tls block is what enforces client certificate authentication — note it's client ca, not just ca. Took me a minute to find that in man httpd.conf.

server "runbsd.me" {
    listen on * tls port 8443
    root "/htdocs/runbsd.me/private"
    tls {
        certificate "/etc/ssl/runbsd.me.fullchain.pem"
        key "/etc/ssl/private/runbsd.me.key"
        client ca "/etc/ssl/dhw-ca.crt"
    }
}

Check syntax and reload

doas httpd -n
doas rcctl reload httpd

No pf changes needed — the default openbsd.amsterdam VM config passes all inbound traffic.

On the Mac — import into Keychain for Safari

Import the client certificate

Double-click dhw-client.p12 on the Desktop. macOS will prompt for the export password, then ask which keychain — choose login.

Import and trust the CA certificate

Double-click dhw-ca.crt. After it imports, find DHW Personal CA in Keychain Access, double-click it, expand Trust, and set When using this certificate to Always Trust. macOS will ask for your login password to confirm.

Test

Navigate to https://runbsd.me:8443/ in Safari. It will prompt you to select a certificate — choose DHW Personal Client. You're in.

Clean up the Desktop

rm ~/Desktop/dhw-client.p12
rm ~/Desktop/dhw-ca.crt

Also delete the .p12 from the zimaboard since it has been imported and is no longer needed:

rm ~/ca/dhw-client.p12

Notes