This guide is similar to
Mozilla's guide for generating self-issued certificates
(last edited in 2014), but it pushes the boundaries a bit more.
Mozilla's guide ironically stresses the importance of X.509 nameConstraints
but then includes in its examples contraints that the current Firefox can't handle.
motivation for an internal root CA
Zero-trust networking has become the norm.
A Kubernetes cluster created with kubeadm
automatically creates a root CA,
used for authenticating the various parts of its cluster to each other.
This is fine for a closed system like a Kubernetes cluster,
but one needs to connect to services in the cluster as well, in authentication domains independent of the cluster's.
For example, running a Web server like Apache on the cluster, one needs the server to authenticate itself to its clients. If the clients are external to the organization, one must obtain a certificate from a CA like Let's Encrypt in order for the certificate to be accepted by clients without producing a security warning. But for clients that are internal to the organization, one can deploy the public key of a self-signed, internal root CA to all the clients, and in doing so avoid the need to obtain from an external entity a certificate for each internal service. Since these external CAs only provide certificates that are valid for a matter of months, this simplifies maintenance of the internal services' certificates and prevents leakage of your internal architecture to the external CA.
creating a self-signed root CA for internal use
Creating a self-signed root CA is straightforward with OpenSSL:
openssl req -x509 -days 3650 -config internal-root-CA.conf -out internal-root-CA.crt
The configuration file internal-root-CA.conf
could be something like:
# OpenSSL config for generating the root CA certificate for the # internal systems. Use as: # # openssl req -x509 -days 3650 -config internal-root-CA.conf -out internal-root-CA.crt # # for a 10-year validity. # See https://docs.openssl.org/1.1.1/man1/req/#configuration-file-format [ req ] # Golang limits this to ≤ 8192 <https://github.com/golang/go/issues/61460> # NSS also limits this to ≤ 8192. # 7680 bits = 256-bit security <https://csrc.nist.gov/pubs/sp/800/57/pt1/r5/final>, Table 2 default_bits = 7680 default_keyfile = internal-root-CA.key prompt = no utf8 = yes encrypt_key = yes # section references distinguished_name = req_distinguished_name x509_extensions = v3_ca # See https://docs.openssl.org/1.1.1/man1/req/#distinguished-name-and-attribute-section-format [ req_distinguished_name ] countryName = US stateOrProvinceName = California organizationName = Acme Corporation commonName = "root CA for Acme internal systems" # See https://docs.openssl.org/1.1.1/man5/x509v3_config/ [ v3_ca ] basicConstraints = critical, CA:true, pathlen:1 subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always, issuer:always keyUsage = critical, keyCertSign, cRLSign nameConstraints = critical, @name_constraints [ name_constraints ] # `directoryName` is not understood by the Go crypto/x509 library as of version 1.24.4. #permitted;dirName = dir_section permitted;email.0 = internal permitted;email.1 = .internal # `dNSName` is not understood by the NSS library in Firefox 128. #permitted;DNS.0 = internal #permitted;DNS.1 = .internal permitted;IP.0 = 192.168.0.0/255.255.0.0 permitted;IP.1 = 10.0.0.0/255.0.0.0 [ dir_section ] countryName = US stateOrProvinceName = California organizationName = Acme Corporation
Note that the order of the nameConstraints
's dirName
fields is significant.
Each certificate is given a X.500 distinguished name (DN), which must be unique,
and is composed of a sequence of relative distinguished names (RDNs)—countryName = US
,
stateOrProvinceName = California
, etc.
ITU X.501 (10/19) specifies in section 9.2
that the SEQUENCE of RelativeDistinguishedName that comprises a DistinguishedName starts with the root.
RFC 4514, section 2.1 specifies
that the string representation reverses that order, placing the root last.
The OpenSSL dirName
and DNS
options
(respectively, the directoryName
and dNSName
X.509 name contraints)
are commented out in this example because the limitations of the libraries (mentioned in the file comments) that are currently in use
would make the certificates practically unusable.
Of course that situation may improve with time.
filename extensions
In my examples, all the files are in PEM format.
But instead of using the .pem
extension everywhere, I use extensions to distinguish their purposes:
.key
for private keys.csr
for certificate signing requests.crt
for public key certificates
and .conf
is for the OpenSSL configurations.
These are pretty commonly used.
Of course the PEM headers also tell what the contents are for, but it's nice to not have to open the file to learn that.
choosing the key algorithm
My overarching advice is: It doesn't matter. If you're just setting up your organization, no one even knows who you are, so no one is trying to break into your systems. Just pick any available algorithm and continue on with your setup. As you gain more knowledge about cryptography, you can replace your certificates with something that engenders more confidence in you. That said, the commands above are designed to give you something reasonable.
Public-key cryptography is based on signing and (symmetric-)key exchange using asymmetric encryption algorithms. In creating a CA certificate, we're concerned with which algorithm we choose. With TLS 1.3—which is what you're most likely going to use exclusively on systems that you control—any key exchange algorithm will work with whatever symmetric-key algorithm you end up negotiating for data encryption; the key exchange algorithm merely needs to be supported by the client's and server's libraries. We're not going to consider post-quantum algorithms here yet, in 2025, mostly because they're not widely implemented or used yet, though they may become necessary…who knows when.
The asymmetric encryption algorithm is in turn based on the hardness of solving an algebraic problem. The two main problems used are (1) semiprime factorization and (2) the discrete logarithm problem (DLP), both of which are computed via multiplication over a finite group. In the case of RSA or Diffie-Hellman (DH), the groups are integer groups whose modulus is a number thousands of bits in length. Elliptic curve cryptography (ECC) utilizes multiplicaton over a different type of finite group: those formed by points on an elliptic curve defined on a finite field. The order of the finite field—on which the arithmetic is performed—has a considerably smaller number of bits than RSA or DH for an equivalent level of security, so calculations are performed faster.
Table 2 of the NIST SP 800-57 Part 1 Rev. 5 “Recommendation for Key Management: Part 1” compares the security levels for key lengths for the various algorithm types. For details on cryptography, read the book Serious Cryptography, Second Ed.
elliptic curves? not yet
Given that we would like to create a root CA certificate that has a long lifetime, so that we don't have to update the internal clients often, we would like to use an algorithm that will remain secure for years, maybe a decade. So we'd like one with greater than 128-bit security. But we'd also like to avoid high computational overhead, which could delay the establishing of a TLS connection. The RSA private-key computations have a computational complexity of roughly , so a 8192-bit key size can lead to delays of around 10 seconds. This makes ECC attractive.
Most elliptic curves defined in cryptography are of the Weierstrass form , where and must be chosen carefully. Most of the time when you hear "elliptic curve" in cryptography, it's referring to one of this form. There are also ECC implementations that use Edwards curves , which have the advantage of resistance to side-channel attacks. Several of those of the Weierstrass form have been supported widely by CAs and browsers, so ECC is available. But there is some suspicion around those ECC implementations, because it later was revealed that the NSA had a hand in choosing the curves' parameters, leading to suspicion that they may be weakened. Among the Edwards curves, we have “curve448”, with 224-bit security, which was designed in a more transparent manner. But as of August 2025, the Edwards curves are not currently widely supported.
So, until Curve448 or Curve25519 are well supported, I'd recommend sticking with RSA.
X.509 v3 certificate settings
The X.509 v3 certificate format is described in RFC 5280. You'll also find some specifics, on what is expected or supported in the leaf certificates, in the CA/Browser Forum's requirements; keep in mind though that this specification is for certificates on the public Internet.
CA hierarchy
Once you have a self-signed CA, you do not want to use its certificate for your Web server. Because your Web server will require access to the private key, if your Web server's security is compromised, then the attacker will also have access to the private key and will thus be able to masquerade as your Web server in a man-in-the-middle attack without any of your internal clients being the wiser. Mitigating such an attack would require updating all of the internal clients with a newly-generated public key.
Naturally, this logic also applies to any other type of server that you may be running internally (VPN, SSH, etc.).
So in order to limit the damage from such an attack, one uses the root CA only for signing another level of certificates. You might simply create a second certificate for your Web server, signed by the root CA. The public key of the root CA would still be made available to all internal clients so that they can validate the Web server's certificate, but if the Web server were compromised, only its certificate would need to be reissued to the Web server, not the root CA's to all the internal clients.
Depending on the needs and the size of the organization, you could extend this hierarchy with another level:
a CA, signed by the root CA, that can be used regularly for signing certificates.
This avoids overexposing the root CA's private key (which is required for signing).
I do this in the above root CA—note the pathlen:1
—and in the following examples.
intermediate CA certificates
Creating further certificates in the hierarchy is done as a two-step process: (1) generate a private key and a CSR, (2) sign the CSR. An intermediate CA is signed by the root CA above:
openssl req -newkey rsa -config internal-web-CA.conf -out internal-web-CA.csr openssl x509 -req -in internal-web-CA.csr -out internal-web-CA.crt -CA internal-root-CA.crt -CAkey internal-root-CA.key -extfile internal-web-CA.conf -extensions v3_ca -days 3650
Here we're creating an intermediate CA that we will use to sign all Web-related leaf certificates.
We may also create other intermediate CAs for signing other domains of leaf certificates: e-mail servers, client certificates, etc.
The configuration file internal-web-CA.conf
could be something like:
# OpenSSL config for generating the intermediate CA certificate for signing # Web-related, internal systems' certificates. Use as: # # openssl req -newkey rsa -config internal-web-CA.conf -out internal-web-CA.csr # openssl x509 -req -in internal-web-CA.csr -out internal-web-CA.crt -CA internal-root-CA.crt -CAkey internal-root-CA.key -extfile internal-web-CA.conf -extensions v3_ca -days 3650 # # for a 10-year validity. [ req ] default_bits = 3072 default_md = sha512 default_keyfile = internal-web-CA.key prompt = no utf8 = yes encrypt_key = yes # section references distinguished_name = req_distinguished_name x509_extensions = v3_ca [ req_distinguished_name ] countryName = US stateOrProvinceName = California organizationName = Acme Corporation commonName = "intermediate CA for Acme's Web-related internal systems" [ v3_ca ] basicConstraints = critical, CA:true, pathlen:0 subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always, issuer:always keyUsage = critical, keyCertSign, cRLSign nameConstraints = critical, @name_constraints [ name_constraints ] permitted;IP.1 = 10.1.0.0/255.255.0.0
Here we're using a smaller RSA modulus on the assumption that this certificate will be rotated more frequently than the root.
leaf certificates
A leaf certificate to be used by a Web server would then be signed by the above intermediate CA for Web servers:
# OpenSSL config for generating the certificate for an internal Web server. Use as: # # openssl req -newkey rsa -config internal-web.conf -out internal-web.csr # openssl x509 -req -in internal-web.csr -out internal-web.crt -CA internal-web-CA.crt -CAkey internal-web-CA.key -extfile internal-web.conf -extensions v3 -days 1095 # # for a 3-year validity. # # No private key password is used, so that no password entry is required at # Web server start-up. [ req ] default_bits = 3072 default_md = sha512 default_keyfile = internal-web.key prompt = no utf8 = yes encrypt_key = no # section references distinguished_name = req_distinguished_name x509_extensions = v3 [ req_distinguished_name ] countryName = US stateOrProvinceName = California organizationName = Acme Corporation commonName = "Acme's internal Web server" [ v3 ] basicConstraints = critical, CA:false subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always, issuer:always subjectAltName = DNS:www.internal, DNS:*.internal keyUsage = critical, digitalSignature, keyEncipherment, keyAgreement extendedKeyUsage = critical, serverAuth
In this example, I don't bother with encrypting the private key. My assumption is that, if the private key file is accessible, the TLS server's security has been breached, so the attacker probably also has access to the (unencrypted) in-memory copy. But if you chose to, you could encrypt the file; that would entail a process for applying the password to decrypt it, of course, which depends on the environment in which you're running the service.
All possible (virtual) host names by which the server may be known
are placed in the subjectAltName
as X.509 dNSName
entries; one level of wildcarding is also allowed.
One of these names used to be placed in the commonName
field,
but that form is deprecated and probably no longer needed/used by any clients.
If the purpose of the certificate is something other than Web serving,
you'll need to adjust the keyUsage
and extendedKeyUsage
accordingly; see the X.509 v3 RFC.
deploy your certificates
On the client side, I can speak for Debian Linux: one copies the root certificate (internal-root-CA.crt
)
to the /usr/local/share/ca-certificates/
directory (and makes the file read-only!), and then runs
update-ca-certificates
.
On a Kubernetes cluster, this can be done across all nodes with a DaemonSet;
see this solution.
You'll probably also need to explicitly add and trust the certificate to your browsers.
On the server side, for the Apache httpd server,
one places a PEM file in the configuration directory (usually /usr/local/apache2/conf/
)
that is a concatenation of the leaf certificate and its chain, all the way down to the root, in that order.
The file is then referenced by
SSLCertificateFile
.
The private key file (internal-web.key
, above) goes in there, too, and is referenced by
SSLCertificateKeyFile
.
verify your certificates
Before you deploy your certificates, you should check that they make sense, at least to OpenSSL, which is widely used to interpret certificates.
openssl verify -verbose -untrusted internal-web-CA.crt internal-web.crt
There are other libraries that parse certificates, so this test isn't comprehensive. Other libraries may have constraints that OpenSSL doesn't impose. So if you're generating a leaf certificate for server authentication, you should set up an initial test deployment of a server and test it against clients like:
- Firefox (uses the NSS library)
- Chrome (uses the OpenSSL-derived BoringSSL library)
No comments:
Post a Comment