Skip to main content

Self-Signed TLS Certificates for gRPC

· 8 min read

Microservice communication needs encryption to keep internal traffic secure, so here is a record of the process for generating self-signed certificates.

Some background first. gRPC defaults to plaintext HTTP/2, which means request bodies between services can be captured and read directly on the internal network. In production, even an internal network cannot be assumed absolutely trustworthy — once a single machine is compromised, sniffing lateral traffic costs the attacker very little. Adding TLS to gRPC is the most direct hardening measure. And for internal services there's no need (and often no way) to request certificates from a public CA; the more common approach is to build your own root certificate and issue a certificate for each service. The whole process can be done with OpenSSL, but there are plenty of details — especially Go's validation rules for certificate fields — worth writing down in full.

Below are the steps to generate self-signed certificates with the OpenSSL command-line tool on Linux or macOS. The process creates a new root certificate (CA), then uses that root certificate to sign a new server certificate.

The certificate hierarchy here has two tiers: the root certificate signs itself and serves as the trust anchor; service certificates are issued by the root. As long as a client trusts the root certificate up front, it can verify any service certificate the root has signed. That's also why only ca.crt needs to be distributed to clients, rather than every individual service certificate.

A quick outline of the overall flow:

Root certificate:

  1. Generate the root private key

  2. Generate the root certificate from the root private key

Service certificate:

  1. Generate the service private key

  2. Generate a service CSR from the service private key

  3. Sign the service .csr file with the root certificate + root certificate key to produce the service certificate .crt file

Generating the Root Certificate's Private Key

# Generate a 2048-bit RSA private key to serve as the root CA's key
openssl genrsa -out ca.key 2048

This command generates a new 2048-bit RSA private key, saved to a file named ca.key.

This key is the root of the entire trust chain — whoever holds it can issue certificates trusted by every client. Guard it carefully and never commit it to a code repository.

Generating the Root Certificate

# Self-sign an X.509 root certificate with the root key, valid for 365 days
openssl req -new -x509 -days 365 -key ca.key -out ca.crt

This command generates a new X.509 root certificate, signed with SHA-256 and valid for 365 days (adjust as you like). The certificate is saved to a file named ca.crt.

The -x509 flag means output a self-signed certificate directly rather than a signing request — this is exactly what "the root certificate signs itself" means.

While running this command, OpenSSL will prompt you for some information to be embedded in the certificate. In most cases you can just press Enter to accept the defaults.

However, the Subject Name or Common Name should be filled in with the name of your microservice.

With the root certificate ready, the next step is generating certificates for individual services. Just repeat the three steps below for each service.

Generating the Server Certificate's Private Key

# Generate a 2048-bit RSA private key for the service
openssl genrsa -out [service-name].key 2048

Similar to the first command; the generated private key is saved to a file named [service-name].key, where [service-name] is your own service's name.

Generating the Server Certificate Signing Request (CSR)

# Generate a certificate signing request (CSR) from the service private key
openssl req -new -key [service-name].key -out [service-name].csr

This command generates a new certificate signing request (CSR) containing the server certificate's public key and some additional information, saved to a file named server.csr.

A CSR is not itself a certificate — it's an application form stating "who I am and what my public key is," which only becomes a usable certificate after the CA signs it.

Again, OpenSSL will prompt you for information to be included in the CSR.

Signing the Server Certificate with the Root Certificate

# Sign the CSR with the root certificate + root key to produce the service certificate
openssl x509 -req -in [service-name].csr -CA ca.crt -CAkey ca.key -CAcreateserial -out [service-name].crt -days 365

This command signs the server certificate with the root certificate; the resulting certificate is saved to a file named server.crt.

The -CAcreateserial flag creates a .srl serial-number file that records the serial numbers of certificates this CA has issued; on subsequent signings it increments automatically with no manual handling needed.

The rootCA.crt, server.crt, and server.key produced by the steps above are now ready for encrypted gRPC communication — configure them on the corresponding server and client sides. The server loads its own certificate and private key; the client loads the root certificate to verify the server's identity.

Custom Fields

In newer versions of Go, the validation and retrieval of certain certificate fields has changed. (Since Go 1.15, the alt_names field of the certificate is used for service name verification.) So we need to specify some custom fields when generating certificates.

In other words, putting the service name in the Common Name alone is no longer enough: Go's TLS client takes the connection's target name during the handshake and matches it against the certificate's SAN (Subject Alternative Name) field — no match, and the handshake fails outright. The certificate must therefore explicitly carry the SAN extension. Since the interactive command line is awkward for filling in these extension fields, we switch to a configuration file.

Create a san.cnf file with the following contents:

[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn

# Certificate subject info, replacing interactive input
[dn]
C = CN
ST = Beijing
L = Beijing
O = 'lynx'
OU = 'lynx'
emailAddress = 'lynx'
CN = [service-name]

# Extensions used at the CSR stage: declare the SAN
[req_ext]
subjectAltName = @alt_names

# The SAN list — these DNS names are what the Go client validates against
[alt_names]
DNS.1 = [service-name]

# Extensions used at the certificate-issuing stage
[v3_ext]
authorityKeyIdentifier=keyid,issuer:always
basicConstraints=CA:FALSE
keyUsage=keyEncipherment,dataEncipherment,digitalSignature
extendedKeyUsage=serverAuth,clientAuth
subjectAltName=@alt_names

In this config, prompt = no tells OpenSSL to skip the interactive questions and read the [dn] section directly; extendedKeyUsage declares both serverAuth and clientAuth, meaning this certificate can be used on the server side and also as a client certificate in mutual-TLS scenarios.

Then bring the config into the certificate-generation steps above.

When generating the service CSR, add the -extensions req_ext -config san.cnf options:

# Reference the config file when generating the CSR, carrying the SAN declared in the req_ext section
openssl req -new -key [service-name].key -out [service-name].csr -extensions req_ext -config san.cnf

When issuing the service certificate, likewise add the -extensions v3_ext -extfile san.cnf options:

# The v3_ext extensions must also be applied at signing time, or the SAN won't make it into the final certificate
openssl x509 -req -in [service-name].csr -CA ca.crt -CAkey ca.key -CAcreateserial -out [service-name].crt -days 365 -extensions v3_ext -extfile san.cnf

Certificates generated this way contain the fields we specified, along with some certificate description information.

warning

The -extensions v3_ext -extfile san.cnf at the signing step is easy to miss. By default, openssl x509 -req does not carry the CSR's extension fields into the final certificate — if the SAN is declared only at the CSR stage, the signed certificate will still have no SAN.

Verifying Certificate Contents

You can check whether the generated certificate really contains the expected fields with:

# Print the certificate in text form to inspect the SAN and other extensions
openssl x509 -in [service-name].crt -text -noout

In the output, focus on whether the X509v3 Subject Alternative Name section contains the expected DNS names, and whether the validity period and issuer are correct. It's worth making this check a fixed step of the process — far easier than debugging a handshake failure after the service is already live.

Pitfalls and Caveats

  1. The target name the client connects with must match a DNS name in the certificate's SAN. If you connect directly by IP, the SAN needs an IP entry declared as IP.1 = ... — a DNS entry will not match.

  2. Once a certificate expires, the gRPC handshake fails outright, and this class of failure tends to surface only on expiry day. Nobody reminds you to renew self-signed internal certificates, so record the expiry dates and rotate ahead of time.

  3. ca.key and each service's .key file are sensitive material. Watch the file permissions and never ship them in images or repositories; clients only need ca.crt.

  4. After changing san.cnf, both the CSR and the signing step must be redone. Re-signing without regenerating the CSR may carry stale subject information.

Wrapping Up

The whole process boils down to one chain of trust: the root key produces the root certificate, the service key produces a CSR, and the root certificate signs the CSR into a service certificate. In Go's gRPC scenario, the key points are writing the SAN field into the certificate via san.cnf, explicitly specifying the extensions at both the CSR and signing stages, and finally verifying with openssl x509 -text. Once these steps are captured in a script, issuing a certificate for a new service is a one-minute job.

COMMENTS