Certificate Authority

Creating a local certificate authority

This post explains how to setup a local CA (certificate authority) from which to issue server certificates for a local network.

Our Setup

At home we also want to have SSL certificates for our servers, so we can leverage encryption of http traffic and turn it into https. Also, some infrastructure servers (i.e. GIT) will rely on https, so it will be a good idea to be prepared. Our setup will consist of three layers:

Root CA <-> Intermediate CA <-> Servers

The general reason we don’t issue server certificates directly from the root CA is that it is 1) rather uncommon to do so and 2) we might end up with the need for more than one CA to issue server certificates in our network (i.e. using lets encrypt)

Root CA

The first thing we need to do is to setup a root CA which more or less is nothing more than a structure of directories on any host. The host actually doesn’t need to be a server and literally can be your laptop. No services need to be started and in reality the root CA often is an air gapped computer.

mkdir /root/ca
cd /root/ca
mkdir certs crl newcerts private
chmod 700 private
touch index.txt
echo 1000 > serial

Now we need to create a config file for openssl

touch caconfig.cnf

The file content is shown below. Please note that the root directory in line 7 of the file needs to match the directory on your drive. If you decide to create the directory somewhere else, please adapt line 7. The same goes for the subdirectories between line 8 and line 21.

[ ca ]
# `man ca`
default_ca = CA_default

[ CA_default ]
# Directory and file locations.
dir               = /root/ca
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.
private_key       = $dir/private/ca.key.pem
certificate       = $dir/certs/ca.cert.pem

# 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        = sha256

name_opt          = ca_default
cert_opt          = ca_default
default_days      = 375
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             = match
stateOrProvinceName     = match
organizationName        = match
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ policy_loose ]
# Allow the intermediate CA to sign a more diverse range of certificates.
# See the POLICY FORMAT section of the `ca` man page.
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

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

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

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

[ req_distinguished_name ]
# See .
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
0.organizationName              = Organization Name
organizationalUnitName          = Organizational Unit Name
commonName                      = Common Name
emailAddress                    = Email Address

# Optionally, specify some defaults.
countryName_default             = DE
stateOrProvinceName_default     = Saxony
localityName_default            = Dresden
0.organizationName_default      = Example
organizationalUnitName_default  = Example Certificate Authority
emailAddress_default            = john.doe@example.com

[ 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_ca ]
# Extensions for a typical intermediate CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
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

[ usr_cert ]
# Extensions for client certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection

[ server_cert ]
# Extensions for server certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth

[ crl_ext ]
# Extension for CRLs (`man x509v3_config`).
authorityKeyIdentifier=keyid:always

[ ocsp ]
# Extension for OCSP signing certificates (`man ocsp`).
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, OCSPSigning

Now it is time to create the root key. Because we are creating a private key for a root CA we should use 4096 bit encryption.

cd /root/ca
openssl genrsa -aes256 -out private/ca.key.pem 4096

You will be prompted to give a password:

Enter pass phrase for ca.key.pem: mysecretpassword
Verifying - Enter pass phrase for ca.key.pem: mysecretpassword

After the certificate is created we should make sure nobody else can read it on the filesystem.

chmod 400 private/ca.key.pem

Now we create a signing request for a certificate that is valid for 20 years. We also implicitly self sign the request:

cd /root/ca
openssl req -config caconfig.cnf -key private/ca.key.pem -new -x509 -days 7300 -sha256 -extensions v3_ca -out certs/ca.cert.pem


When we are being prompted for the password, we shall give the private key password from above.

Enter pass phrase for ca.key.pem: mysecretpassword
You are about to be asked to enter information that will be incorporated
into your certificate request.
-----
Country Name (2 letter code) [XX]:DE
State or Province Name []:Saxony
Locality Name []: Dresden
Organization Name []:Example
Organizational Unit Name []:Example Certificate Authority
Common Name []:Example Root CA
Email Address []:john.doe@example.com

The public certificate can be read by everybody, as long as nobody can write it. If desired we can also use openssl to marvel at our fresh certificate and verify it.

chmod 444 certs/ca.cert.pem
openssl x509 -noout -text -in certs/ca.cert.pem

Intermediate CA

Same as for the root CA, we now setup an intermediate CA. The intermediate CA could be at the same host, but it really doesn’t have to be though.

mkdir /root/ca/ca-intermediate
cd /root/ca/ca-intermediate
mkdir certs crl csr newcerts private
chmod 700 private
touch index.txt
echo 1000 > serial
echo 1000 > crlnumber

Again, we need to create a config file for openssl

touch ca-intermediate.cnf

The file content is shown below. Not much has changed compared to the root CA. The policy we apply is now loose (see line 32) and we copy possible extensions (see line 35). Again, the root directory in line 7 of the file needs to match the directory on your drive. If you decide to create the directory somewhere else, please adapt line 7. The same goes for the subdirectories between line 8 and line 21.

[ ca ]
# `man ca`
default_ca = CA_default

[ CA_default ]
# Directory and file locations.
dir               = /root/ca/ca-intermediate
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.
private_key     = $dir/private/intermediate.key.pem
certificate     = $dir/certs/intermediate.cert.pem

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

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

name_opt          = ca_default
cert_opt          = ca_default
default_days      = 375
preserve          = no
policy            = policy_loose

# <<< I M P O R T A N T >>>
copy_extensions   = copy

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

[ policy_loose ]
# Allow the intermediate CA to sign a more diverse range of certificates.
# See the POLICY FORMAT section of the `ca` man page.
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

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

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

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

[ req_distinguished_name ]
# See .
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
0.organizationName              = Organization Name
organizationalUnitName          = Organizational Unit Name
commonName                      = Common Name
emailAddress                    = Email Address

# Optionally, specify some defaults.
countryName_default             = DE
stateOrProvinceName_default     = Saxony
localityName_default            = Dresden
0.organizationName_default      = Example
organizationalUnitName_default  = Example Certificate Authority
emailAddress_default            = john.doe@example.com

[ 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_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

[ usr_cert ]
# Extensions for client certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection

[ server_cert ]
# Extensions for server certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth

[ crl_ext ]
# Extension for CRLs (`man x509v3_config`).
authorityKeyIdentifier=keyid:always

[ ocsp ]
# Extension for OCSP signing certificates (`man ocsp`).
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, OCSPSigning

Next we can create the intermediate private key (please note that we are still in the ca-intermediate folder):

openssl genrsa -aes256 -out private/intermediate.key.pem 4096

We are being prompted to give a password:

Enter pass phrase for intermediate.key.pem: anothersecretpassword
Verifying - Enter pass phrase for intermediate.key.pem: anothersecretpassword

We should make sure that nobody can read the private key:

chmod 400 private/intermediate.key.pem

As a next step we now create a signing request:

openssl req -config ca-intermediate.cnf -new -sha256 -key private/intermediate.key.pem -out csr/intermediate.csr.pem

As we do so, we are being prompted for the password that protects the private key of our intermediate certificate authority.

Enter pass phrase for intermediate.key.pem: anothersecretpassword
You are about to be asked to enter information that will be incorporated
into your certificate request.
-----
Country Name (2 letter code) [XX]:DE
State or Province Name []:Saxony
Locality Name []:Dresden
Organization Name []:Example
Organizational Unit Name []:Example Certificate Authority
Common Name []:Example Intermediate CA
Email Address []:john.doeh@example.com

We have successfully created a certificate signing request. Now let’s go back to the Root CA and sign the request. Let’s make sure we use the Root CA’s openssl config for doing that:

cd /root/ca
openssl ca -config caconfig.cnf -extensions v3_intermediate_ca -days 3650 -notext -md sha256 -in ca-intermediate/csr/intermediate.csr.pem -out ca-intermediate/certs/intermediate.cert.pem

The password we are being prompted for is the password of the Root CA – makes sense, because the Root CA is asked to sign something.

Enter pass phrase for ca.key.pem: mysecretpassword
Sign the certificate? [y/n]: y

It should be allowed that everybody can read the public certificate. At the same time nobody should be allowed to write it.

chmod 444 ca-intermediate/certs/intermediate.cert.pem

We could verify the intermediate certificate with the following command:

openssl x509 -noout -text -in ca-intermediate/certs/intermediate.cert.pem

However, so much more important is to verify the chain of the intermediate certificate against the root certificate:

openssl verify -CAfile certs/ca.cert.pem ca-intermediate/certs/intermediate.cert.pem

intermediate.cert.pem: OK

On that note… let’s create a certificate chain that we can later on hand out to web servers (i.e. apache has an option for that):

cat ca-intermediate/certs/intermediate.cert.pem certs/ca.cert.pem > ca-intermediate/certs/ca-chain.cert.pem
chmod 444 ca-intermediate/certs/ca-chain.cert.pem

Server Certificate

Finally, we can issue a server certificate from our intermediate CA. At first, create the server key (make sure it is 2048 bit). As we are creating a certificate for a service (i.e. Nginx or Apache) we should skip the -aes256 option as we otherwise would create a private key with a password and would need to enter the password every time we start the service.

cd /root/ca/ca-intermediate
openssl genrsa -out private/myserver.example.com.key.pem 2048
chmod 400 private/myserver.example.com.key.pem

It makes a lot of sense to create a config file for openssl!

cd /root/ca/ca-intermediate
touch myserver.cnf

And here is why: look at lines 72, 96 and 99 until 101. Our server will have more than just one simple domain name service entry. It might be available under the full name myserver.example.com but also under the short name myserver. We need alternative DNS entries for the server certificate.

[ ca ]
# `man ca`
default_ca = CA_default

[ CA_default ]
# Directory and file locations.
dir               = /root/ca/ca-intermediate
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.
private_key     = $dir/private/intermediate.key.pem
certificate     = $dir/certs/intermediate.cert.pem

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

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

name_opt          = ca_default
cert_opt          = ca_default
default_days      = 375
preserve          = no
policy            = policy_loose

# <<< I M P O R T A N T >>>
# Extension copying option: use with caution.
copy_extensions   = copy

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

[ policy_loose ]
# Allow the intermediate CA to sign a more diverse range of certificates.
# See the POLICY FORMAT section of the `ca` man page.
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

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

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

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

# <<< I M P O R T A N T >>>
req_extensions      = v3_req

[ req_distinguished_name ]
# See .
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
0.organizationName              = Organization Name
organizationalUnitName          = Organizational Unit Name
commonName                      = Common Name
emailAddress                    = Email Address

# Optionally, specify some defaults.
countryName_default             = DE
stateOrProvinceName_default     = Saxony
localityName_default            = Dresden
0.organizationName_default      = Example
organizationalUnitName_default  = Example Web Services
emailAddress_default            = john.doe@example.com

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
# <<< I M P O R T A N T >>>
subjectAltName = @alt_names

# <<< I M P O R T A N T >>>
[ alt_names ]
DNS.0 = myserver.example.com
DNS.1 = myserver

[ 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_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

[ usr_cert ]
# Extensions for client certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection

[ server_cert ]
# Extensions for server certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth

[ crl_ext ]
# Extension for CRLs (`man x509v3_config`).
authorityKeyIdentifier=keyid:always

[ ocsp ]
# Extension for OCSP signing certificates (`man ocsp`).
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, OCSPSigning

Now we create the signing request (make sure you’re in the intermediate ca folder and use the special server config as it contains the SAN):

openssl req -config myserver.cnf -key private/myserver.example.com.key.pem -new -sha256 -out csr/myserver.example.com.csr.pem

There should be no password prompt, as our private key is unprotected.

You are about to be asked to enter information that will be incorporated
into your certificate request.
-----
Country Name (2 letter code) [XX]:DE
State or Province Name []:Saxony
Locality Name []:Dresden
Organization Name []:Example
Organizational Unit Name []:Example Web Services
Common Name []:myserver.example.com
Email Address []:john.doe@example.com

Now we sign the request with the intermediate CA and write protect it:

openssl ca -config ca-intermediate.cnf -extensions server_cert -days 375 -notext -md sha256 -in csr/myserver.example.com.csr.pem -out certs/myserver.example.com.cert.pem
chmod 444 certs/myserver.example.com.cert.pem

You can verify the certificate:

openssl x509 -noout -text -in certs/myserver.example.com.cert.pem

However, very important would be to create a bundle that chains the intermediate certificate with the server certificate:

cat certs/myserver.example.com.cert.pem certs/intermediate.cert.pem > certs/myserver.example.com.bundle.pem

Last but not least lets verify the chain of the server certificate against the root certificate:

cd /root/ca
openssl verify -CAfile certs/ca.cert.pem ca-intermediate/certs/myserver.example.com.bundle.pem

myserver.example.com.cert.pem: OK

…all done!