Technology & Life

Summary
A comprehensive guide to configuring your own Certificate Authority (CA) on OpenBSD
Published
Reading time
14 min
Tags
, ,

This is a guide for configuring a local CA (Certificate Authority) on OpenBSD systems. This allows one to issue trusted certificates for individual devices and services on a local network.

  1. Creating the root CA
  2. Creating an intermediate CA, signed by the root
  3. Creating new private keys, certificate signing requests (CSRs), and certificates for local homelab applications
  4. Tips, best practices, and troubleshooting

Be sure to replace my-ca in the example commands with your own CA hostname. Alternatively, search/replace this file to update the commands.

This can be a helpful way to understanding the components and naming conventions:

The Root CA is the top of the certification structure. The keys must be kept private.

mkdir -p /etc/ssl/private && chmod 700 /etc/ssl/private
mkdir -p /etc/ssl/my-ca/root/{certs,crl,newcerts,private}
chmod 700 /etc/ssl/my-ca/root/private

We'll use 1000 as the starting index for both the root and intermediate databases. The crlnumber file is used if certificates are revoked in the future.

touch /etc/ssl/my-ca/root/index.txt
echo 1000 > /etc/ssl/my-ca/root/serial
echo 1000 > /etc/ssl/my-ca/root/crlnumber

Create /etc/ssl/my-ca/root/openssl.cnf. We need to specify configuration that is not in the default openssl.cnf.

# /etc/ssl/my-ca/root/openssl.cnf
[ req ]
distinguished_name = req_distinguished_name
prompt = yes

[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
countryName_default             = US
stateOrProvinceName             = State or Province Name
stateOrProvinceName_default     = State
localityName                    = Locality Name
localityName_default            = City
0.organizationName              = Organization Name
0.organizationName_default      = Home Lab
organizationalUnitName          = Organizational Unit Name
organizationalUnitName_default  = IT
commonName                      = Common Name
commonName_max                  = 64

[ ca ]
default_ca = CA_default

[ CA_default ]
dir             = /etc/ssl/my-ca/root      # root CA base directory
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.pem
certificate     = $dir/certs/ca.crt.pem

default_md      = sha256
policy          = policy_loose
email_in_dn     = no
copy_extensions = copy

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

[ server_cert ]
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth

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

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

Notes

openssl genrsa -aes256 -out /etc/ssl/my-ca/root/private/ca.key.pem 4096
chmod 400 /etc/ssl/my-ca/root/private/ca.key.pem

openssl req -new -x509 -days 7300 -sha256 \
  -key /etc/ssl/my-ca/root/private/ca.key.pem \
  -extensions v3_ca \
  -config /etc/ssl/my-ca/root/openssl.cnf \
  -out /etc/ssl/my-ca/root/certs/ca.crt.pem

You can now import /etc/ssl/my-ca/root/certs/ca.crt.pem into your browser/OS trust stores which will allow all future issued certificates to be properly trusted.

You may also want to convert it from PEM to DER format only because Windows will recognize it and assign a handler.

openssl x509 -outform der -in /etc/ssl/my-ca/root/certs/ca.crt.pem -out /etc/ssl/my-ca/root/certs/ca.crt

The intermediate CA is used for day-to-day signing tasks and allows the Root CA to remain offline.

mkdir -p /etc/ssl/my-ca/intermediate/{certs,crl,csr,newcerts,private}
chmod 700 /etc/ssl/my-ca/intermediate/private

touch /etc/ssl/my-ca/intermediate/index.txt
echo 1000 > /etc/ssl/my-ca/intermediate/serial
echo 1000 > /etc/ssl/my-ca/intermediate/crlnumber

Create/edit /etc/ssl/my-ca/intermediate/openssl.cnf:

# /etc/ssl/my-ca/intermediate/openssl.cnf
[ req ]
distinguished_name = req_distinguished_name
prompt = yes

[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
countryName_default             = US
stateOrProvinceName             = State or Province Name
stateOrProvinceName_default     = State
localityName                    = Locality Name
localityName_default            = City
0.organizationName              = Organization Name
0.organizationName_default      = Home Lab
organizationalUnitName          = Organizational Unit Name
organizationalUnitName_default  = IT
commonName                      = Common Name
commonName_max                  = 64

[ ca ]
default_ca = CA_default

[ CA_default ]
dir             = /etc/ssl/my-ca/intermediate
certs           = $dir/certs
crl_dir         = $dir/crl
new_certs_dir   = $dir/newcerts
database        = $dir/index.txt
serial          = $dir/serial
private_key     = $dir/private/intermediate.key.pem
certificate     = $dir/certs/intermediate.crt.pem
default_md      = sha256
policy          = policy_loose
copy_extensions = copy
unique_subject  = no

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

[ server_cert ]
basicConstraints = CA:FALSE
nsCertType = server
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = server.local  # Without `-addext`, these must be changed per signature
IP.1 = 192.168.0.1

openssl genrsa -aes256 -out /etc/ssl/my-ca/intermediate/private/intermediate.key.pem 4096
chmod 400 /etc/ssl/my-ca/intermediate/private/intermediate.key.pem

openssl req -new -sha256 \
  -key /etc/ssl/my-ca/intermediate/private/intermediate.key.pem \
  -out /etc/ssl/my-ca/intermediate/csr/intermediate.csr.pem

openssl ca -config /etc/ssl/my-ca/root/openssl.cnf \
  -extensions v3_intermediate_ca \
  -days 3650 -notext -md sha256 \
  -in /etc/ssl/my-ca/intermediate/csr/intermediate.csr.pem \
  -out /etc/ssl/my-ca/intermediate/certs/intermediate.crt.pem
chmod 444 /etc/ssl/my-ca/intermediate/certs/intermediate.crt.pem

This will update the database for the first time:

Sign the certificate? [y/n]:y
Write out database with 1 new entries
Data Base Updated

openssl verify -CAfile /etc/ssl/my-ca/root/certs/ca.crt.pem \
    /etc/ssl/my-ca/intermediate/certs/intermediate.crt.pem

Concatenate the new intermediate certificate + root certificate to create a root certificate chain. This ca-chain.pem chain is for verification (openssl verify). For serving HTTPS, we will concatenate app cert + intermediate only as clients will already have the root cert.

# Chain for verification (intermediate + root)
cat /etc/ssl/my-ca/intermediate/certs/intermediate.crt.pem \
    /etc/ssl/my-ca/root/certs/ca.crt.pem \
    > /etc/ssl/my-ca/intermediate/certs/ca-chain.pem

The following are ways to create, request, sign, verify, and deploy a TLS certificates signed by the local CA.

Two main approaches for application keys:

Use central management for system services, per-app for containerized/isolated apps.

Pro-tip: replace my-server with your server name then paste the commands.

  1. Generate private key for application
openssl genrsa -out /etc/ssl/private/my-server.key.pem 4096
chmod 400 /etc/ssl/private/my-server.key.pem
  1. Create CSR (Certificate Signing Request) for application
openssl req -new -sha256 \
  -key /etc/ssl/private/my-server.key.pem \
  -out /etc/ssl/my-ca/intermediate/csr/my-server.csr.pem
  1. Sign application CSR with intermediate CA. Make sure you edit the sample Alt Name configuration in the command below
openssl ca -config /etc/ssl/my-ca/intermediate/openssl.cnf \
  -extensions server_cert -days 825 -notext -md sha256 \
  -in /etc/ssl/my-ca/intermediate/csr/my-server.csr.pem \
  -out /etc/ssl/my-ca/intermediate/newcerts/my-server.crt.pem
chmod 444 /etc/ssl/my-ca/intermediate/newcerts/my-server.crt.pem
  1. Verify application certificate
openssl verify -CAfile /etc/ssl/my-ca/intermediate/certs/ca-chain.pem \
    /etc/ssl/my-ca/intermediate/newcerts/my-server.crt.pem
  1. Create certificate chain
# Chain for serving (leaf + intermediate, no root)
cat /etc/ssl/my-ca/intermediate/newcerts/my-server.crt.pem \
    /etc/ssl/my-ca/intermediate/certs/intermediate.crt.pem \
    > /etc/ssl/my-ca/intermediate/certs/my-server-chain.crt.pem
  1. Ready to deploy files The file files are ready for use in the application:
  1. Set permissions once files are in place:
chmod 400 my-application.key.pem
chmod 444 my-application-chain.crt.pem

  1. On the app server, create a new private key in the application directory
cd application-directory/
openssl genrsa -out my-application.key.pem 4096
  1. On the app server, create a CSR (Certificate Signing Request) using the new private key
openssl req -new -sha256 \
  -key my-application.key.pem \
  -out my-application.csr.pem
  1. On the app server, send the request to the CA
scp my-application.csr.pem user@my-ca:/tmp/
  1. On the CA server, sign the CSR using the intermediate CA. Make sure you edit the sample Alt Name configuration in the command below
mv /tmp/my-application.csr.pem /etc/ssl/my-ca/intermediate/csr/my-application.csr.pem
openssl ca -config /etc/ssl/my-ca/intermediate/openssl.cnf \
  -extensions server_cert -days 825 -notext -md sha256 \
  -in /etc/ssl/my-ca/intermediate/csr/my-application.csr.pem \
  -out /etc/ssl/my-ca/intermediate/newcerts/my-application.crt.pem
  1. Verify application certificate
openssl verify -CAfile /etc/ssl/my-ca/intermediate/certs/ca-chain.pem \
    /etc/ssl/my-ca/intermediate/newcerts/my-application.crt.pem
  1. On the CA server, concatenate the newly signed certificate + the intermediate certificate to create a full chain for serving with Node This does not include the root CA, but clients will need the root CA installed anyway. Note that the earlier created ca-chain.pem includes both intermediate + root.
# Chain for serving (leaf + intermediate, no root)
cat /etc/ssl/my-ca/intermediate/newcerts/my-application.crt.pem \
    /etc/ssl/my-ca/intermediate/certs/intermediate.crt.pem \
    > /etc/ssl/my-ca/intermediate/certs/my-application-chain.crt.pem
  1. On the CA server, copy the signed certificate chain back to the app server
scp /etc/ssl/my-ca/intermediate/certs/my-application-chain.crt.pem user@app-server:/tmp/
  1. On the app server, store the private key and certificate in a tls directory, then load the private key and certificate
mv my-application.key.pem tls/
mv /tmp/my-application-chain.crt.pem tls/
  1. Set permissions
chmod 400 tls/my-application.key.pem
chmod 444 tls/my-application-chain.crt.pem
chmod 700 tls/
  1. Load the key and certificate in the application code
const options = {
    key: fs.readFileSync('tls/my-application.key.pem'),
    cert: fs.readFileSync('tls/my-application-chain.crt.pem')
};

This way Node presents both the leaf and the intermediate cert to the client. Clients already have the root CA installed and thus can validate the entire chain.

Tips

  1. Go to Node > System > Certificates > Click Upload Custom Certificate button.
  2. Paste key and certificate. It does verify that they are both populated and matching.
  3. The service should restart itself and begin presenting the new certificate.

  1. Go to Settings > General > SSL certificate.
  2. Upload the certificate chain and key.
  3. Restart the Portainer container.

Alternatively, place the files in /etc/ssl and reference them in the Portainer docker-compose.yml:

services:
  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    ports:
      - 9443:9443
    environment:
      - REGISTRY_HTTP_TLS_CERTIFICATE=/certs/portainer.crt
      - REGISTRY_HTTP_TLS_KEY=/certs/portainer.key
    volumes:
      - portainer_data:/data
      - /var/run/docker.sock:/var/run/docker.sock
      - /etc/ssl/portainer.key:/certs/portainer.key
      - /etc/ssl/certs/portainer.crt:/certs/portainer.crt
    restart: unless-stopped
volumes:
  portainer_data:
networks:
  default:
    name: portainer_default
    driver: bridge

Tips

Simply copy the files to the server, place them in the correct location and name, and restart the service.

sudo mkdir -p /etc/cockpit/ws-certs.d/backup/
sudo mv /etc/cockpit/ws-certs.d/0-self* /etc/cockpit/ws-certs.d/backup/
sudo cp /tmp/*.key.pem /etc/cockpit/ws-certs.d/0-centos.key  # <-- must end in ".key"
sudo cp /tmp/*.crt.pem /etc/cockpit/ws-certs.d/0-centos.cert # <-- must end in ".cert"
sudo chown root:root /etc/cockpit/ws-certs.d/*.{key,cert}
sudo chmod 644 /etc/cockpit/ws-certs.d/0-centos.cert
sudo chmod 640 /etc/cockpit/ws-certs.d/0-centos.key
sudo restorecon -Rv /etc/cockpit/ws-certs.d/ # <-- no output if previous steps were done
sudo systemctl restart cockpit

If the service doesn't start, check the journal:

journalctl -u cockpit.service -n 50 --no-pager

Tips

Problem

Using configuration from /etc/ssl/openssl.cnf variable lookup failed for ca::default_ca 4295208342000:error:0EFFF06C:configuration file routines:CRYPTO_internal:no value:/usr/src/lib/libcrypto/conf/conf_lib.c:167:group=ca name=default_ca

Solution

  1. Use the provided local openssl.cnf and reference it as an argument -config /etc/ssl/my-ca/root/openssl.cnf

Problem

ERROR: adding extensions in section v3_intermediate_ca 13145493656048:error:22FFF07B:X509 V3 routines:CRYPTO_internal:unable to get issuer keyid:/usr/src/lib/libcrypto/x509/x509_akey.c:203: 13145493656048:error:22FFF080:X509 V3 routines:CRYPTO_internal:error in extension:/usr/src/lib/libcrypto/x509/x509_conf.c:96:name=authorityKeyIdentifier, value=keyid:always,issuer

Solutions

  1. Make sure your root CA cert has subjectKeyIdentifier. You may need to regenerate the root CA if SKI is missing.
  2. (Shortcut) Drop keyid:always from authorityKeyIdentifier in openssl.cnf

Testing

  1. You should see the SKI (Subject Key Identifier) in the output: openssl x509 -in /etc/ssl/my-ca/root/certs/ca.crt.pem -noout -text

Check what a server responds with:

openssl s_client -connect server.local:3000 -showcerts

Check if the name was properly set:

openssl x509 -in /etc/ssl/my-ca/intermediate/certs/my-application-chain.crt.pem -noout -text | grep -A1 "Subject Alternative Name"

Root CA File Purpose / Description
/etc/ssl/my-ca/root/private/ca.key.pem Root CA private key (the crown jewels). Keep offline or on encrypted media.
/etc/ssl/my-ca/root/certs/ca.crt.pem Root CA self-signed certificate. Installed into trust stores.
/etc/ssl/my-ca/root/index.txt Database tracking issued certificates.
/etc/ssl/my-ca/root/serial Next serial number to issue.
/etc/ssl/my-ca/root/crlnumber Tracks CRL numbers if you issue revocations.
/etc/ssl/my-ca/root/newcerts/ Where OpenSSL stores issued certificates by serial number.
/etc/ssl/my-ca/root/openssl.cnf Root CA OpenSSL configuration (used to sign intermediate).

Intermediate CA File Description
/etc/ssl/my-ca/intermediate/private/intermediate.key.pem Intermediate CA private key. Kept online, but protected.
/etc/ssl/my-ca/intermediate/certs/intermediate.crt.pem Intermediate CA certificate signed by the root.
/etc/ssl/my-ca/intermediate/certs/ca-chain.pem Concatenated intermediate + root certificates (used for validation).
/etc/ssl/my-ca/intermediate/index.txt Database for issued end-entity certs.
/etc/ssl/my-ca/intermediate/serial Next serial number to issue for intermediates.
/etc/ssl/my-ca/intermediate/crlnumber CRL tracking for the intermediate.
/etc/ssl/my-ca/intermediate/newcerts/ Auto-populated directory of issued certs.
/etc/ssl/my-ca/intermediate/csr/intermediate.csr.pem CSR sent from intermediate to root for signing.
/etc/ssl/my-ca/intermediate/openssl.cnf Config for intermediate CA operations.

Sample NodeJS File Description
/tmp/my-application.csr.pem CSR created from private key. Can delete after signing or move to intermediate/csr/
/etc/ssl/my-ca/intermediate/certs/my-application.crt.pem App server certificate signed by intermediate.
/etc/ssl/my-ca/intermediate/certs/my-application-chain.crt.pem App server cert leaf + intermediate CA cert (used for serving).
my-application.key.pem Deployed Node app private key.
my-application-chain.crt.pem Deployed cert chain (leaf + intermediate). Used by Node.

Trusted Root CA File Description
/etc/ssl/my-ca/root/certs/ca.crt.pem The only file that needs to be installed into the OS/browser trust store.

Back to top ↑