OpenBSD Root CA Setup
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.
Overview
- Creating the root CA
- Creating an intermediate CA, signed by the root
- Creating new private keys, certificate signing requests (CSRs), and certificates for local homelab applications
- 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.
Terminology
- CA = Certificate Authority (the signing entity)
- crt/cert = Certificate (public, signed document)
- CSR = Certificate Signing Request (application for a certificate)
- Intermediate CA = Delegates signing authority from root (used daily)
- key = Private key (secret, never shared)
- PEM = Privacy Enhanced Mail (Base64-encoded format)
- Root CA = Top-level CA (kept offline)
- SAN = Subject Alternative Name (DNS names/IPs in a cert)
Passport Analogy
This can be a helpful way to understanding the components and naming conventions:
my.key.pem= your personal, private signature (cannot be forged, must be kept secret).my.csr.pem= CSR (Certificate Signing Request) = your passport application.ca.crt.pem= CA cert = the government's signing authority.my.crt.pem= your actual passport, signed by the government.ca-chain.pem= intermediate+root = the chain of custody proving the government itself is real.
1. Root CA (kept offline or on encrypted media)
The Root CA is the top of the certification structure. The keys must be kept private.
1.1. Create root structure
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
1.2. Initialize root database
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
1.3. Create root openssl.cnf
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
- Use the new config whenever using
opensslby using-config /etc/ssl/my-ca/root/openssl.cnf. - We will create a similar but separate
openssl.cnffile for the intermediate CA.
1.4. Generate root CA key (keep offline and well protected)
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
1.5. Generate self-signed root CA cert (valid for 20 years)
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
1.6. Accept new root CA
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
2. Intermediate CA (used for daily signing)
The intermediate CA is used for day-to-day signing tasks and allows the Root CA to remain offline.
2.1. Create intermediate structure
mkdir -p /etc/ssl/my-ca/intermediate/{certs,crl,csr,newcerts,private}
chmod 700 /etc/ssl/my-ca/intermediate/private
2.2. Initialize intermediate database
touch /etc/ssl/my-ca/intermediate/index.txt
echo 1000 > /etc/ssl/my-ca/intermediate/serial
echo 1000 > /etc/ssl/my-ca/intermediate/crlnumber
2.3. Create intermediate openssl.cnf
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
2.4. Generate intermediate key
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
2.5. Create CSR (Certificate Signing Request) for intermediate
openssl req -new -sha256 \
-key /etc/ssl/my-ca/intermediate/private/intermediate.key.pem \
-out /etc/ssl/my-ca/intermediate/csr/intermediate.csr.pem
2.6. Sign intermediate CA with root CA (valid for 10 years)
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
2.7. Verify intermediate certificate after signing
openssl verify -CAfile /etc/ssl/my-ca/root/certs/ca.crt.pem \
/etc/ssl/my-ca/intermediate/certs/intermediate.crt.pem
2.8. Create a chain file for verification
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
3. Certificate deployment
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:
- Central management: Generate in
/etc/ssl/private/(Generic example) - Per-app: Generate in app directory (NodeJS example)
Use central management for system services, per-app for containerized/isolated apps.
3.1. Server certificate example (central management)
Pro-tip: replace my-server with your server name then paste the commands.
- 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
- 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
- Sign application CSR with intermediate CA. Make sure you edit the sample Alt Name configuration in the command below
- The
-addextparameter requires OpenSSL 1.1.1+- If you have it, you can skip editing
openssl.cnfand specify the parameters via CLI:-addext "subjectAltName=DNS:myserver.local,IP:10.0.1.2" \
- Without
-addext, one must edit theopenssl.cnf"alt_names" section per signature to set hostname and IP (yes, tedious) and omit-addext:mg /etc/ssl/my-ca/intermediate/openssl.cnf
- If you have it, you can skip editing
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
- 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
- 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
- Ready to deploy files The file files are ready for use in the application:
- Certificate:
/etc/ssl/my-ca/intermediate/newcerts/my-application.crt.pem - Private key:
/etc/ssl/private/my-application.key.pem - Chain:
/etc/ssl/my-ca/intermediate/certs/my-application-chain.crt.pem(application certificate + intermediate for serving)
- Set permissions once files are in place:
chmod 400 my-application.key.pem
chmod 444 my-application-chain.crt.pem
3.2. NodeJS certificate example (per-app)
- On the app server, create a new private key in the application directory
cd application-directory/
openssl genrsa -out my-application.key.pem 4096
- 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
- On the app server, send the request to the CA
scp my-application.csr.pem user@my-ca:/tmp/
- On the CA server, sign the CSR using the intermediate CA. Make sure you edit the sample Alt Name configuration in the command below
- Note: The
-addextparameter requires OpenSSL 1.1.1+ and OpenBSD may not support it yet
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
- 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
- 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.pemincludes 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
- 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/
- On the app server, store the private key and certificate in a
tlsdirectory, then load the private key and certificate
mv my-application.key.pem tls/
mv /tmp/my-application-chain.crt.pem tls/
- Set permissions
chmod 400 tls/my-application.key.pem
chmod 444 tls/my-application-chain.crt.pem
chmod 700 tls/
- 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
- Keep server.key mode 400, owned by the user running your Node process
- Put certs in a
tls/dir that only your app can read (700)
3.3. Proxmox
- Go to Node > System > Certificates > Click
Upload Custom Certificatebutton. - Paste key and certificate. It does verify that they are both populated and matching.
- The service should restart itself and begin presenting the new certificate.
3.4. Portainer
- Go to Settings > General > SSL certificate.
- Upload the certificate chain and key.
- 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
- You may want to disable HTTP access after verifying the certificates are working.
3.5. CentOS Cockpit
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
- The naming convention for the files is:
[prefix].[cert|key] - Both the
.certand.keyfiles must share the same filename prefix (e.g.,0-centos.certand0-centos.key) - The directory is sorted and the first/top items are used.
- The certificate must contain at least two PEM certificates in a chain (server cert + intermediates) if using a private CA.
- Reference: Cockpit TLS Configuration
4. Best Practices & References
4.1. Best Practices
- Root CA private key: keep offline, only used to sign intermediates.
- Intermediate CA private key: encrypted, kept online but well-guarded.
- Use 400 for private keys, 444 for certs, 700 for CA private dirs.
- 400 (read-only, owner): Private keys must stay secret
- 444 (read-only, all): Certificates are public by nature, but shouldn't be modified
- Regular backups of /etc/ssl/my-ca (encrypted, ideally offsite).
- Intermediate CA cert rotation policy
- The default is 10 years (3650 days).
- For higher security, consider a shorter intermediate lifetime and rotate it every few years.
- Subject Alternative Names (SANs) are required by modern browsers (CN alone is deprecated).
- Root CA cert: No SAN needed (it's not serving anything).
- Intermediate CA cert: No SAN needed (it's not serving anything).
- Application certs: SAN is always required for HTTPS servers.
- Cert lifetimes: 825 days max for public trust compatibility (2.25 years).
- This is based on Apple/Google's maximum certificate lifetime policies.
4.2. References
5. Troubleshooting
5.1. Missing default_ca
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
- Use the provided local
openssl.cnfand reference it as an argument-config /etc/ssl/my-ca/root/openssl.cnf
5.2. Unable to get issuer
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
- Make sure your root CA cert has
subjectKeyIdentifier. You may need to regenerate the root CA if SKI is missing. - (Shortcut) Drop
keyid:alwaysfrom authorityKeyIdentifier inopenssl.cnf
Testing
- 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
5.3. Check certificates
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"
6. Appendix
6.1. Root CA files
| 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). |
6.2. Intermediate CA files
| 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. |
6.3. Sample NodeJS files
| 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. |
6.4. Trusted Root CA files (OS/browsers)
| 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. |