I wanted to run a small private Certificate Authority for some of my internal services. Since these aren’t reachable from the internet, and some of them are on network segments without internet connectivity, using a public ACME CA like Let’s Encrypt was inconvenient. On the other hand, if I run my own private CA and the keys get compromised, it could be used to MITM all my internet traffic. While that’s unlikely to happen, I decided to look for a better option.
It turns out that the idea of a “limited purpose” Certificate Authority is not
new. RFC 5280 provides for something
called “Name Constraints”, which allow an X.509 CA to have a scope limited to
certain names, including the parent domains of the certificates issued by the
CA. For example, a host constraint of .example.com
allows the CA to issue
certificates for anything under .example.com
, but not any other host. For
other hosts, clients will fail to validate the chain.
This hasn’t always been supported by TLS libraries and browsers, but all current browsers do support Name Constraints. Consequently, this is an approach to narrow the risks associated with a CA compromise for hosts other than those covered by the constraints in the CA certificate.
So it turns out that it’s not super simple to set this up. You need to configure the correct OpenSSL extensions for the CA and the certificates, and the easiest way is to pass them in in an ini file.
First, generate your private key and certificate signing request for the CA. I did mine with a 4096-bit RSA key:
1
2
openssl genrsa -aes256 -out ca.key.pem 4096
openssl req -new -key ca.key.pem -extensions v3_ca -batch -out ca.csr -utf8 -subj '/C=US/O=Example/OU=CA'
Now create a configuration file with the extensions and self-sign the CSR, using SHA-256 for the hashes:
1
2
3
4
5
6
7
cat <<EOF >caext.ini
basicConstraints = critical, CA:TRUE
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
nameConstraints = critical, permitted;DNS:.example.com
EOF
openssl x509 -req -sha256 -days 365 -in ca.csr -signkey ca.key.pem -extfile caext.ini -out ca.crt
You now have a CA that’s constrained to only validate when it signs certificates
for hosts under .example.com
. We’ll need to setup the serial file as well to
record the current serial of the certificates:
1
echo 1000 > ca.srl
Now let’s create and sign a certificate using our new CA. We’ll go for
test.example.com
in this case:
1
2
3
4
5
6
7
8
9
10
11
12
13
openssl genrsa -aes256 -out test.key.pem 2048
openssl req -new -key test.key.pem -days 365 -extensions v3_ca -batch -out test.csr -utf8 -subj '/CN=test.example.com'
cat <<'EOF' >certext.ini ✘ 1
basicConstraints = critical,CA:false
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
nsCertType = server
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = ${ENV::CERT_SAN}
EOF
CERT_SAN=DNS:test.example.com openssl x509 -req -sha256 -days 365 -in test.csr -CAkey ca.key.pem -CA ca.crt -out test.crt -extfile certext.ini
We use an environment variable to pass in the subjectAltName
specification for
the extension.
Now we can use the openssl verify
command to check the certificate against the
CA:
1
2
openssl verify -CAfile ca.crt test.crt
test.crt: OK
We can also test that the constraints work as we intend by creating another
certificate, this time for a domain under example.org
, not example.com
.
This should violate the name constraints and fail to verify.
1
2
3
4
5
6
openssl req -new -key test.key.pem -days 365 -extensions v3_ca -batch -out test2.csr -utf8 -subj '/CN=test2.example.org'
CERT_SAN=DNS:test2.example.org openssl x509 -req -sha256 -days 365 -in test2.csr -CAkey ca.key.pem -CA ca.crt -out test2.crt -extfile certext.ini
openssl verify -CAfile ca.crt test2.crt
CN = test2.example.org
error 47 at 0 depth lookup: permitted subtree violation
error test2.crt: verification failed
As expected, this CA is no good for a domain not in the permitted subtree of
.example.com
.
This allows you to create a CA for one or more domains that won’t be accepted by browsers for other domains. You can safely establish a CA for your internal tools, private networks, etc., and not worry that a compromised CA will compromise all your communications. Of course, you should still take precautions with your CA key, such as using an HSM, storing it offline, or encrypting it appropriately.