OpenVPN: Built a certification authority from scratch with openssl

Hi,

these are the steps to build your own CA (Certification Authority) and all requiered certificates for a OpenVPN instance (Client and Server) on Linux.

Define your environment. Always set these variables in the shell before executing openssl commands. Adjust it to your needs.

# Root Directory of the CA
export CA_ROOT_DIRECTORY=${HOME}/openvpn/CA
# openssl binary
export OPENSSL_BIN=`which openssl`
# Your CA Name, no spaces in Name are allowed!!
export CA_NAME=MyOpenVPNCA
# The common directory
export CA_COMMON_DIR=${CA_ROOT_DIRECTORY}/common
# Directory for Client Certificates
export CA_CLIENTS_DIR=${CA_ROOT_DIRECTORY}/Clients
# Directory for Server Certificate
export CA_SERVER_DIR=${CA_ROOT_DIRECTORY}/Server
# export CA_CERTS_DIR=${CA_ROOT_DIRECTORY}/certs
# Your CA Password
export CA_PASSWORD="YourSecurePassword"
# Default Key Bitsize
export KEY_BITSIZE=4096
# Cerdificate Lifetime in Days
export CERTIFICATE_LIFETIME=3650
# Files
# The Environment variable where openssl looking for its config
export OPENSSL_CONF=${CA_COMMON_DIR}/openssl.cfg
# x509_v3 Server Extensions
export OPENSSL_CONF_X509_V3_EXT_SERVER=${CA_COMMON_DIR}/x509v3_server.ext
# x509_v3 Client Extensions
export OPENSSL_CONF_X509_V3_EXT_CLIENT=${CA_COMMON_DIR}/x509v3_client.ext
# The CRL list (Client revocation)
export CA_CRL=${CA_COMMON_DIR}/crl.pem

Initialise the CA

# Create all directories
mkdir -p "${CA_COMMON_DIR}"
mkdir -p "${CA_CLIENTS_DIR}"
mkdir -p "${CA_SERVER_DIR}"
# Create empty database index
touch "${CA_ROOT_DIRECTORY}/index.txt"
# Set first serial
echo 1 > "${CA_ROOT_DIRECTORY}/serial"

Create a default openvpn config and alter the sections req_distinguished_name to your environment. Simply copy the following lines to your shell.

cat > $OPENSSL_CONF <<EOF
dir                 = .

[ ca ]
default_ca      = $CA_NAME            # The default ca section

####################################################################
[ $CA_NAME ]

# dir             = ./CA              # Where everything is kept
dir             = ${CA_ROOT_DIRECTORY}
certs           = \$dir/certs            # Where the issued certs are kept
crl_dir         = \$dir/crl              # Where the issued crl are kept
database        = \$dir/index.txt        # database index file.
#unique_subject = no                    # Set to 'no' to allow creation of
                                        # several ctificates with same subject.
new_certs_dir   = \$dir/newcerts         # default place for new certs.

certificate     = \$dir/common/ca.cer       # The CA certificate
serial          = \$dir/serial           # The current serial number
# crlnumber       = \$dir/crlnumber        # the current crl number
                                        # must be commented out to leave a V1 CRL
crl             = \$dir/crl/crl.pem          # The current CRL
private_key     = \$dir/common/ca.key    # The private key
RANDFILE        = \$dir/common/.rand    # private random number file

default_days    = ${CERTIFICATE_LIFETIME}                  # how long to certify for
default_crl_days= 30                    # how long before next CRL
default_md  = default                   # use public key default MD
preserve    = no                        # keep passed DN ordering
policy      = policy_match

[ policy_match ]
countryName                 = match
stateOrProvinceName         = match
organizationName            = match
organizationalUnitName      = optional
commonName                  = supplied
emailAddress                = optional

[ req ]
default_bits                = $KEY_BITSIZE          # Size of keys
default_keyfile             = key.pem       # name of generated keys
default_md                  = sha256        # message digest algorithm
string_mask                 = nombstr       # permitted characters
distinguished_name          = req_distinguished_name
req_extensions              = v3_req

[ req_distinguished_name ]
# Variable name               Prompt string
#-------------------------    ----------------------------------
0.organizationName          = MyCompany
organizationalUnitName      = MyOrganisation
emailAddress                = your-e-mail@MyOrganisation.org
emailAddress_max            = 40
localityName                = Nuremburg
stateOrProvinceName         = Frankonia
countryName                 = DE
countryName_min             = 2
countryName_max             = 2
commonName                  = openvpnhost.MyOrganisation.org
commonName_max              = 64

# Default values for the above, for consistency and less typing.
# Variable name                 Value
#------------------------       ------------------------------
0.organizationName_default      = MyCompany
localityName_default            = Nurenburg
stateOrProvinceName_default     = Frankonia
countryName_default             = DE

[ v3_server ]
basicConstraints       = CA:FALSE
nsCertType             = server
nsComment              = "Server Certificate for $CA_NAME"
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer:always
extendedKeyUsage       = serverAuth

[ v3_client ]
basicConstraints       = CA:FALSE
nsComment              = "Client Certificate for $CA_NAME"
nsCertType             = client
extendedKeyUsage       = clientAuth
keyUsage = digitalSignature, keyAgreement

[ v3_ca ]
basicConstraints                = CA:TRUE
subjectKeyIdentifier            = hash
authorityKeyIdentifier          = keyid:always,issuer:always

[ v3_req ]
basicConstraints                = CA:FALSE
subjectKeyIdentifier            = hash

EOF

Builting the selfsigned Certification Authority with a 4096Bit RSA Key

# Creating a Selfsigned CA with a RSA 4096Bit Key
$OPENSSL_BIN req -new -x509 -days ${CERTIFICATE_LIFETIME} -extensions v3_ca -newkey rsa:${KEY_BITSIZE} -keyout "${CA_COMMON_DIR}/ca.key" -out "${CA_COMMON_DIR}/ca.cer" -batch  -passout pass:$CA_PASSWORD

Creating the certificate for the OpenVPN server


# Create Server Key
$OPENSSL_BIN genrsa -out "${CA_SERVER_DIR}/server.key" ${KEY_BITSIZE} -aes256

# Create s signing request
$OPENSSL_BIN req -nodes -new -key "${CA_SERVER_DIR}/server.key" -out "${CA_SERVER_DIR}/server.req" -extensions v3_server -batch -subj "/C=DE/ST=Frankonia/L=Nurenburg/O=YourCompany/OU=Department/CN=OpenVPN Server/emailAddress=yourMail@yourCompany.org"

# Sign server certificate
$OPENSSL_BIN x509 -req -days ${CERTIFICATE_LIFETIME} -extfile $OPENSSL_CONF -extensions v3_server -in "${CA_SERVER_DIR}/server.req" -CA "${CA_COMMON_DIR}/ca.cer" -CAkey "${CA_COMMON_DIR}/ca.key" -CAcreateserial -out "${CA_SERVER_DIR}/server.cer" -passin pass:$CA_PASSWORD
#  Check if x509 v3 extensions are set
$OPENSSL_BIN x509 -text -in "${CA_SERVER_DIR}/server.cer"|grep "X509v3 extensions" -A 15
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            Netscape Cert Type:
                SSL Server
            Netscape Comment:
                Server Certificate for MyOpenVPNCA
            X509v3 Subject Key Identifier:
                71:AD:6D:4A:37:9B:A6:4A:7C:70:59:14:98:54:C5:6E:4B:36:6E:E3
            X509v3 Authority Key Identifier:
                keyid:35:34:FB:FC:32:5D:09:57:9B:F7:97:91:6F:91:E4:A0:74:98:E7:0C
                DirName:/O=MyCompany/L=Nurenburg/ST=Frankonia/C=DE
                serial:BE:6B:FA:A0:67:AD:2A:6F

            X509v3 Extended Key Usage:
                TLS Web Server Authentication

# Write Client Key and Certificate and CA Certificate to a pkcs12 File with no password
$OPENSSL_BIN pkcs12 -password pass: -export -in "${CA_SERVER_DIR}/server.cer" -inkey "${CA_SERVER_DIR}/server.key" -certfile "${CA_COMMON_DIR}/ca.cer" -out "${CA_SERVER_DIR}/server.p12"

Create the crl revocation list. This list contains all certificates you have revoked.

$OPENSSL_BIN ca -gencrl -out ${CA_CRL} -passin pass:$CA_PASSWORD

Create an diffie hellmann file for a secure KeyExchange

$OPENSSL_BIN gendh -out "${CA_SERVER_DIR}/dh.pem" 2048

Copy the pkcs12 ( ${CA_SERVER_DIR}/server.p12 ) , the CRL ( ${CA_CRL} ) and Diffie Hellmann ( ${CA_SERVER_DIR}/dh.pem ) files to your OpenVPN Server and add this options to the config file:

pkcs12     "$PATH_TO/server.p12"
dh         "$PATH_TO/dh.pem"
crl-verify "$PATH_TO/crl.pem"
mode server
tls-server

OpenVPN Clients

Do these steps for each Client you want to connect


# Set the Client Computername. This is the common name
export CLIENT_COMPUTERNAME=ClientComputerName1

Create a the key


# Create a 4096Bit RSA key
$OPENSSL_BIN genrsa -out "${CA_CLIENTS_DIR}/${CLIENT_COMPUTERNAME}.key" ${KEY_BITSIZE} -aes256

# Signing request
$OPENSSL_BIN req -new -key "${CA_CLIENTS_DIR}/${CLIENT_COMPUTERNAME}.key" -out "${CA_CLIENTS_DIR}/${CLIENT_COMPUTERNAME}.reg" -extensions v3_client -batch -subj "/C=DE/ST=Frankonia/L=Nurenburg/O=YourCompany/OU=Department/CN=${CLIENT_COMPUTERNAME}/emailAddress=yourMail@yourCompany.org"

# Sign client certificate</pre>
<pre>
$OPENSSL_BIN x509 -req -days ${CERTIFICATE_LIFETIME} -extfile $OPENSSL_CONF -extensions v3_client -in "${CA_CLIENTS_DIR}/${CLIENT_COMPUTERNAME}.reg" -CA "${CA_COMMON_DIR}/ca.cer" -CAkey "${CA_COMMON_DIR}/ca.key" -CAcreateserial -out "${CA_CLIENTS_DIR}/${CLIENT_COMPUTERNAME}.cer" -extensions v3_client -passin pass:$CA_PASSWORD

# Export the bundle to a pkcs12 file
$OPENSSL_BIN pkcs12 -password pass: -export -in "${CA_CLIENTS_DIR}/${CLIENT_COMPUTERNAME}.cer" -inkey "${CA_CLIENTS_DIR}/${CLIENT_COMPUTERNAME}.key" -certfile "${CA_COMMON_DIR}/ca.cer" -out "${CA_CLIENTS_DIR}/${CLIENT_COMPUTERNAME}.p12"

# Diffie Hellmann
$OPENSSL_BIN gendh -out "${CA_CLIENTS_DIR}/${CLIENT_COMPUTERNAME}.dh" 2048

Copy the pkcs12 and diffie hellmann file to the Client and add

dh "$Path_To/clientname.dh"
pkcs12 "$Path_To/clientname.p12"
ns-cert-type server

tls-client

to the openvpn config.

Revoke a Client certificate

export CLIENT_COMPUTERNAME=ClientComputerName1

# Revoke a certificate
$OPENSSL_BIN ca -revoke "${CA_CLIENTS_DIR}/${CLIENT_COMPUTERNAME}.cer" -passin pass:$CA_PASSWORD

# Generate a new CRL
$OPENSSL_BIN ca -gencrl -out ${CA_CRL} -passin pass:$CA_PASSWORD

and copy the CRL to the openvpn server

To renew the CA public key create an signing request. If you have built your CA within two steps and still have the original csr then you can specify it by parameter -in

$OPENSSL_BIN req -new -key "${CA_CLIENTS_DIR}/${CLIENT_COMPUTERNAME}.key" -out "${CA_COMMON_DIR}/renew_csr.pem" -extensions v3_ca -passin pass:$CA_PASSWORD 

And create the new certificate

$OPENSSL_BIN x509 -req -days $CERTIFICATE_LIFETIME -in "${CA_COMMON_DIR}/renew_csr.pem" -signkey "${CA_CLIENTS_DIR}/${CLIENT_COMPUTERNAME}.key" -out "${CA_COMMON_DIR}/renewed_ca.cer" -passin pass:$CA_PASSWORD

Michael

9 thoughts on “OpenVPN: Built a certification authority from scratch with openssl”

  1. Hi!

    Greate tutorial and about the only one i could find that went through the whole process with OpenSSL instead of EasyRSA (which I dont want to use). There is just one thing missing. In the openssl config, the row “keyUsage = digitalSignature, keyEncipherment” is missing from the “[ v3_server ]” section.

    After adding that, everything worked!

    1. Hi,
      Thank you for your nice article. I also cofirm the above argument, otherwise you are going to get
      Certificate does not have key usage extension from OpenVPN

  2. Thanks!

    In addition to the changes suggested in previous comments, I needed to
    1) remove the options “-aes256” in both the “# Create Server Key” and “# Create a 4096Bit RSA key” steps.
    2) edit the “subj” option in the client section so that it matched what was in the variable names at the top of the instructions. Perhaps it would be possible to instead change the PolicyMatch settings that are currently “match” to be “optional”.

    1. It’s not necessary to remove -aes256. Just write before the BITSIZE:
      $OPENSSL_BIN genrsa -out “${CA_SERVER_DIR}/server.key” -aes256 ${KEY_BITSIZE}
      $OPENSSL_BIN genrsa -out “${CA_CLIENTS_DIR}/${CLIENT_COMPUTERNAME}.key” -aes256 ${KEY_BITSIZE}

  3. It does not appear to update the index and the serial file from the code given. I’m in a doubt whether I’m doing something wrong, but as it was mostly copy paste, I doubt it. I do not have any functionality issues aside from this.

    1. I found it! The serial does end up being the ANSI format openssl expects, but ASCII.
      I changed it to echo “01” > “${CA_ROOT_DIRECTORY}/serial”

Leave a Reply Cancel reply