Setting up your own Certificate Authority (CA) for internal services might seem like overkill, but it’s the only way to get truly custom certificates that your clients will trust without paying a fortune.

Let’s say you have a fleet of internal APIs, each with a unique hostname like api-user.internal.company.com and api-billing.internal.company.com. You want them all to serve traffic over HTTPS, signed by something your internal machines trust. You could use self-signed certs, but then every client needs to be manually configured to trust that specific certificate, which is a nightmare to manage. With a private CA, you generate a root certificate, tell all your internal clients to trust that root, and then use the root to sign certificates for each of your services. Now, any certificate signed by your root is automatically trusted.

Here’s how you’d get a root CA going using openssl, the ubiquitous crypto toolkit.

First, create a directory to hold your CA’s secrets and configuration.

mkdir ~/my-private-ca
cd ~/my-private-ca

Now, generate a private key for your root CA. This key is critical. If it’s compromised, your entire internal PKI is toast. Keep it offline or heavily secured. We’ll use a strong 4096-bit RSA key.

openssl genrsa -aes256 -out ca.key 4096

You’ll be prompted for a passphrase. Make it strong! This key will be encrypted with AES-256.

Next, create the root certificate itself. This is the self-signed certificate that will form the basis of trust. We’ll give it a common name (CN) that clearly identifies it as your internal CA.

openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt -subj "/C=US/ST=California/L=San Francisco/O=MyCompany Internal/CN=MyCompany Internal Root CA"
  • -x509: Outputs a self-signed certificate.
  • -new: Generates a new certificate request.
  • -nodes: Skips encrypting the private key with a passphrase (this is applied to the key itself when you generate it with genrsa, not the certificate).
  • -key ca.key: Specifies the private key to use.
  • -sha256: Uses SHA-256 for the signature hash.
  • -days 3650: Sets the certificate validity to 10 years (3650 days).
  • -out ca.crt: The output file for the root certificate.
  • -subj "/C=US/ST=California/L=San Francisco/O=MyCompany Internal/CN=MyCompany Internal Root CA": Provides subject information directly. Adjust these fields to your organization’s details.

After this, you’ll have ca.key (your encrypted private key) and ca.crt (your root certificate). You need to distribute ca.crt to all your internal machines and configure them to trust it. On Linux systems, this typically involves placing the .crt file in /etc/pki/ca-trust/source/anchors/ and then running sudo update-ca-trust. On Windows, you’d import it into the "Trusted Root Certification Authorities" store via certmgr.msc.

Now, let’s issue a certificate for an internal service, say api-user.internal.company.com. First, we need a Certificate Signing Request (CSR) for this service.

Create a configuration file for the CSR, server.csr.cnf:

[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no

[req_distinguished_name]
C = US
ST = California
L = San Francisco
O = MyCompany Internal
CN = api-user.internal.company.com

[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = api-user.internal.company.com
DNS.2 = api-user

This config specifies the common name (CN) and, crucially, Subject Alternative Names (SANs). SANs are where you list all hostnames and IP addresses the certificate should be valid for. DNS.1 is the primary name, and DNS.2 is a common shorthand you might use internally.

Generate the private key for the service:

openssl genrsa -out api-user.key 2048

Now, generate the CSR using the key and the config file:

openssl req -new -key api-user.key -out api-user.csr -config server.csr.cnf

With the CSR and your root CA key/certificate, you can now sign the CSR to issue the service’s certificate. You’ll need a configuration file for the CA itself, ca.cnf, to tell openssl how to sign.

[ca]
default_ca = CA_default

[CA_default]
dir = ~/my-private-ca/
certs = $dir/certs
crl_dir = $dir/crl
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/ca.crt
serial = $dir/serial
private_key = $dir/ca.key
RANDFILE = $dir/private/.rand

x509_extensions = v3_ca
default_days = 365
default_crl_days = 30
default_md = sha256

preserve = no
policy = policy_match

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

[v3_ca]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth

Before signing, ensure you have the necessary directories and files for the CA. Initialize the index file and serial number.

touch ~/my-private-ca/index.txt
echo 1000 > ~/my-private-ca/serial
mkdir -p ~/my-private-ca/newcerts

Now, sign the CSR. You’ll need the passphrase for your ca.key.

openssl ca -config ~/my-private-ca/ca.cnf -in api-user.csr -out api-user.crt -extensions v3_ca -days 365 -notext
  • -ca ~/my-private-ca/ca.cnf: Uses your CA configuration.
  • -in api-user.csr: The input Certificate Signing Request.
  • -out api-user.crt: The output signed certificate.
  • -extensions v3_ca: Applies the extensions defined in the [v3_ca] section of ca.cnf.
  • -days 365: Sets the certificate validity to 1 year.
  • -notext: Does not output the text version of the certificate.

You’ll be prompted for your CA private key passphrase.

You now have api-user.crt, which is the signed certificate for your service, and api-user.key, which is the private key for that service. You’ll deploy both of these to your api-user service.

The critical, often overlooked, part of this entire process is ensuring your ca.crt is distributed and trusted by all clients that will connect to your internal services. If even one client doesn’t trust the root CA, it will reject connections from any service signed by it, leading to "certificate untrusted" errors that are impossible to debug without knowing the root cause.

Want structured learning?

Take the full Tls-ssl course →