If you have a lab like me, you will probably need to serve some of your services through Web. And speaking of web, we cannot ignore one critical aspect – Security.

The easiest way to secure your websites is to use TLS. However, most easy-to-get certificates for websites are meant for public domains, not for IPs, not to mention internal IPs. Furthermore, you might not feel comfortable exposing your service to your CA.

Yes, all the certificates signed by an authorized authority is publicly available for check at a public website, https://crt.sh. If anyone intentionally get your root domain, that guy would be able to see all the sub-domains that you have ever obtained a certificate, which you might not feel comfortable with.

That’s when a Private CA comes into play. If you sign your own certificate, that wouldn’t be a problem, as long as you can install the self-signed Root on all of your devices.

Prerequisites

You will need a computer dedicated for signing your certificate. This is optional, but it’s highly suggested. It’s not recommended to connect it to the Internet to ensure safety. Even something like a Raspberry Pi, a single-board computer should handle the job. Make sure it’s Airgapped (a computer that will not be connected to the Internet anymore) after you have finished all the software configurations.

A latest copy of OpenSSL installed. Most GNU/Linux operating systems will have this installed by default, but if it’s not, just install it using your package manager. Any recent version will be fine.

USB stick(s), for backing up your keys.

Setting up

Root

Since RSA is already terribly slow compared with ECC, and literally all the devices today supports elliptic curve cryptography now, there’s no obvious reason not to use ECC.

Technically, any device manufactured after the year of 2009 should support ECC.

Let’s assume that you are going to perform all your operation in the folder /media/causb, this folder will be called $HOMEPATH from now own.

Copy and paste the following content to $HOMEPATH/openssl.cnf

# OpenSSL CA configuration file.

[ ca ]
# `man ca`
default_ca = CA_root

[ CA_root ]
# Directory and file locations.
dir               = /media/causb         # CHANGE HERE
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/newcerts
database          = $dir/index.txt
serial            = $dir/serial
RANDFILE          = $dir/private/.rand

# The root key and root certificate.
# Match names with Smallstep naming convention
private_key       = $dir/root_encrypted.key    # CHANGE AFTER YOU GENERATE YOUR KEY
certificate       = $dir/root.crt

# For certificate revocation lists.
crlnumber         = $dir/crlnumber
crl               = $dir/crl/ca.crl.pem
crl_extensions    = crl_ext
default_crl_days  = 30

# SHA-1 is deprecated, so use SHA-2 instead.
default_md        = sha384

name_opt          = ca_default
cert_opt          = ca_default
default_days      = 36500
preserve          = no
policy            = policy_strict

[ policy_strict ]
# The root CA should only sign intermediate certificates that match.
# See the POLICY FORMAT section of `man ca`.
countryName             = optional
organizationName        = optional
stateOrProvinceName	= optional
localityName		= optional
organizationName	= optional
commonName              = supplied

[ req ]
# Options for the `req` tool (`man req`).
default_bits        = 384
distinguished_name  = req_distinguished_name
string_mask         = utf8only

# SHA-1 is deprecated, so use SHA-2 instead.
default_md          = sha384

# Extension to add when the -x509 option is used.
x509_extensions     = v3_ca

[ req_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
countryName			= Country Name (2 letter code)
countryName_default		= US
countryName_min			= 2
countryName_max			= 2

stateOrProvinceName		= State or Province Name (full name)
stateOrProvinceName_default	= California

localityName			= Locality Name (eg, city)
localityName_default		= Los Angeles

0.organizationName		= Organization Name (eg, company)
0.organizationName_default	= ACME

# we can do this but it is not needed normally :-)
#1.organizationName		= Second Organization Name (eg, company)
#1.organizationName_default	= World Wide Web Pty Ltd

commonName			= Common Name (e.g. server FQDN or YOUR name)
commonName_max			= 128


[ v3_ca ]
# Extensions for a typical CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ v3_intermediate_signing_ca ]
# Extensions for a typical intermediate CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
# Allow up to 2 sub-CAs to sign from it
basicConstraints = critical, CA:true, pathlen:2
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ v3_intermediate_ca ]
# Extensions for a typical intermediate CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ v3_req ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:false
keyUsage = critical, digitalSignature, keyEncipherment, nonRepudiation
extendedKeyUsage = serverAuth, clientAuth

Create those directories to store your keys and certificates.

mkdir -p $HOMEPATH/{certs,crl,newcerts,private}
touch $HOMEPATH/index.txt
echo 1420 > $HOMEPATH/serial

Now, create the root certificate.

cd $HOMEPATH # Make your working directory as the current directory.
openssl ecparam -name secp384r1 -genkey -noout -out root.key # Generate a private key using SECP 384 curve, a 384-bit curve.
openssl ec -in root.key -aes_256_cbc -out root_encrypted.key # Optional, encrypt your root key. Recommended. 
# Later all of our commands will be based on the encrypted root. If yours are not encrypted, manually change the "root_encrypted" to "root".
openssl -req -config ./openssl.cnf -key ./root_encrypted.key -days 36526 -new -x509 -sha384 -extensions v3_ca -out ./root.crt
# The above command creates a Root certificate that valids for 100 years.

Intermediate

You won’t want to sign all of your client certificate using your root certificate, which is both inconvenient and insecure, thus we are going to generate an intermediate one.

openssl ecparam -name secp384r1 -out intermediate.key
openssl req -config ./openssl.cnf -new -sha384 -key ./intermediate.key -out intermediate.csr
# Generate a CSR, the certificate signing request, which gives its common name, etc.
# Sign the CSR with the root key.
openssl ca -config ./openssl.cnf -keyfile ./root_encrypted.key -cert ./root.crt -extensions v3_intermediate_ca -days 3650 -md sha384 -notext -in ./intermediate.csr -out intermediate.crt

End-user certificate

Now, you have generated your root certificate and intermediate certificate. You’ll need to generate the certificate for the end user.

Server / Client Authentication

This is the most common one, securing the communication between you and the web server.

However, this is also the most different one.

There is a thing called Subject Alternative Name (SAN). What does that mean? That means this certificate can be used on multiple hostnames instead of just the one in the common name. This is a must-have. If this is not existing, modern web browsers will say this is untrusted.

Okay, let’s generate the certificates. Remember to replace the corresponding arguments, especially the subjectAltName

openssl ecparam -name prime256v1 -noout -genkey -out user.key
openssl req -config ./openssl.cnf -new -sha256 -key ./user.key -out user.csr -addext "subjectAltName = DNS:example.com,DNS:*.example.com,IP:1.1.1.1,IP:1.0.0.1"
openssl ca -config ./openssl.cnf -keyfile ./intermediate.key -cert ./intermediate.crt -extensions v3_req -days 3650 -md sha256 -notext -in ./user.csr -out user.crt

If everything goes well, you should now get a fully functional certificate.

The output may look like this.

╭─frank@frank-confidential ~/.ca 
╰─$ openssl ecparam -name prime256v1 -genkey -noout -out user.key
╭─frank@frank-confidential ~/.ca 
╰─$ openssl req -config ./openssl.cnf -new -sha256 -key ./user.key -out user.csr -addext "subjectAltName = IP:10.10.0.200"               
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [US]:CN
State or Province Name (full name) [California]:Jiangsu
Locality Name (eg, city) [Los Angeles]:Liyang
Organization Name (eg, company) [ACME]:ORWTMC, LLC.
Common Name (e.g. server FQDN or YOUR name) []:10.10.0.200
╭─frank@frank-confidential ~/.ca 
╰─$ openssl ca -config ./openssl.cnf -keyfile ./intermediate.key -cert ./intermediate.crt -extensions v3_req -days 3650 -md sha256 -notext 
-in user.csr -out user.crt
Using configuration from ./openssl.cnf
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number: 5153 (0x1421)
        Validity
            Not Before: Mar 16 14:40:38 2024 GMT
            Not After : Mar 14 14:40:38 2034 GMT
        Subject:
            countryName               = CN
            stateOrProvinceName       = Jiangsu
            localityName              = Liyang
            organizationName          = ORWTMC, LLC.
            commonName                = 10.10.0.200
        X509v3 extensions:
            X509v3 Subject Key Identifier: 
                48:D8:F6:6C:0B:DF:C7:8D:51:23:FC:58:C5:7E:06:A4:DD:0B:16:59
            X509v3 Authority Key Identifier: 
                4D:63:F7:93:50:DE:F4:78:17:90:B3:50:F3:68:71:E6:8D:9D:84:CE
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Key Usage: critical
                Digital Signature, Non Repudiation, Key Encipherment
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
Certificate is to be certified until Mar 14 14:40:38 2034 GMT (3650 days)
Sign the certificate? [y/n]:y


1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated

Now let’s generate the Full-Chain certificate.

cat user.crt intermediate.crt root.crt > fullchain.crt

Alright! You are done!

References

Self-Hosted TRUST with your own Certificate Authority!, archived in Internet Archive on December 12, 2023