Skip to content

Instantly share code, notes, and snippets.

@tlhakhan
Created May 22, 2026 14:00
Show Gist options
  • Select an option

  • Save tlhakhan/01e855d2b57863bd0c1bfba265fd1c74 to your computer and use it in GitHub Desktop.

Select an option

Save tlhakhan/01e855d2b57863bd0c1bfba265fd1c74 to your computer and use it in GitHub Desktop.
OpenSSL command examples to generate certificates for my etcd cluster

etcd TLS Certificates with OpenSSL

Full walkthrough for generating a CA, server, peer, and client certificates for etcd using the modern genpkey/pkey subcommands and .ext config files.


Overview

Role File prefix EKU SANs
CA ca n/a (signs others) n/a
Server server serverAuth required
Peer peer serverAuth + clientAuth required
Client client clientAuth optional

1. CA

ca.ext

[req]
x509_extensions    = v3_ca
distinguished_name = req_distinguished_name
prompt             = no

[req_distinguished_name]
O  = homelab
CN = etcd-ca

[v3_ca]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always, issuer
basicConstraints       = critical, CA:TRUE
keyUsage               = critical, keyCertSign, cRLSign

x509_extensions (not req_extensions) is the right key when openssl req is invoked with -x509 to produce a self-signed cert. prompt = no makes OpenSSL read DN fields directly from this section instead of prompting.

Commands

# Generate private key
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out ca.key
chmod 600 ca.key

# Inspect private key
openssl pkey -in ca.key -text -noout

# Extract and view public key
openssl pkey -in ca.key -pubout -out ca.pub
openssl pkey -pubin -in ca.pub -text -noout

# Self-signed CA cert (10 years)
openssl req -new -x509 -days 3650 -sha256 \
  -key ca.key \
  -config ca.ext \
  -out ca.crt

# View cert
openssl x509 -in ca.crt -text -noout

2. Server

server.ext

Used for the etcd server certificate. Requires serverAuth only. Edit [alt_names] to match your node's IPs and DNS names.

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

[req_distinguished_name]
O  = homelab
CN = etcd-server

[v3_req]
basicConstraints       = CA:FALSE
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always
keyUsage               = critical, keyEncipherment, digitalSignature
extendedKeyUsage       = serverAuth
subjectAltName         = @alt_names

[alt_names]
DNS.1 = localhost
DNS.2 = etcd-node-1
IP.1  = 127.0.0.1
IP.2  = 192.168.1.10

Commands

# Generate private key
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out server.key
chmod 600 server.key

# Inspect private key
openssl pkey -in server.key -text -noout

# View public key (inline, no file)
openssl pkey -in server.key -pubout | openssl pkey -pubin -text -noout

# Generate CSR
openssl req -new \
  -key server.key \
  -config server.ext \
  -out server.csr

# Inspect CSR — verify SANs are present
openssl req -in server.csr -text -noout

# Sign with CA
openssl x509 -req -days 365 -sha256 \
  -in server.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -extensions v3_req -extfile server.ext \
  -out server.crt

# View signed cert
openssl x509 -in server.crt -text -noout

3. Peer

peer.ext

Used for etcd peer-to-peer communication. Requires both serverAuth and clientAuth because each peer acts as both a server and a client to other peers.

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

[req_distinguished_name]
O  = homelab
CN = etcd-peer

[v3_req]
basicConstraints       = CA:FALSE
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always
keyUsage               = critical, keyEncipherment, digitalSignature
extendedKeyUsage       = serverAuth, clientAuth
subjectAltName         = @alt_names

[alt_names]
DNS.1 = localhost
DNS.2 = etcd-node-1
IP.1  = 127.0.0.1
IP.2  = 192.168.1.10

Commands

# Generate private key
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out peer.key
chmod 600 peer.key

# Inspect private key
openssl pkey -in peer.key -text -noout

# View public key (inline, no file)
openssl pkey -in peer.key -pubout | openssl pkey -pubin -text -noout

# Generate CSR
openssl req -new \
  -key peer.key \
  -config peer.ext \
  -out peer.csr

# Inspect CSR — verify SANs and dual EKU are present
openssl req -in peer.csr -text -noout

# Sign with CA
openssl x509 -req -days 365 -sha256 \
  -in peer.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -extensions v3_req -extfile peer.ext \
  -out peer.crt

# View signed cert
openssl x509 -in peer.crt -text -noout

4. Client

client.ext

Used for client certificates (e.g. kube-apiserver → etcd). No SANs required; clientAuth EKU is sufficient. The CN identifies the client to etcd's RBAC.

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

[req_distinguished_name]
O  = homelab
CN = etcd-client

[v3_req]
basicConstraints       = CA:FALSE
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always
keyUsage               = critical, keyEncipherment, digitalSignature
extendedKeyUsage       = clientAuth

Commands

# Generate private key
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out client.key
chmod 600 client.key

# Inspect private key
openssl pkey -in client.key -text -noout

# View public key (inline, no file)
openssl pkey -in client.key -pubout | openssl pkey -pubin -text -noout

# Generate CSR
openssl req -new \
  -key client.key \
  -config client.ext \
  -out client.csr

# Inspect CSR
openssl req -in client.csr -text -noout

# Sign with CA
openssl x509 -req -days 365 -sha256 \
  -in client.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -extensions v3_req -extfile client.ext \
  -out client.crt

# View signed cert
openssl x509 -in client.crt -text -noout

5. Verification

# Verify each cert against the CA
openssl verify -CAfile ca.crt server.crt
openssl verify -CAfile ca.crt peer.crt
openssl verify -CAfile ca.crt client.crt

# Confirm cert and key match (the two fingerprints must be identical)
openssl x509 -in server.crt -pubkey -noout | openssl dgst -sha256
openssl pkey  -in server.key -pubout       | openssl dgst -sha256

# Check SANs, EKU, and key usage on a cert
openssl x509 -in server.crt -noout -text \
  | grep -E -A3 "Subject Alternative|Extended Key Usage|Key Usage|Basic Constraints"

# Confirm CA cert is actually marked as a CA
openssl x509 -in ca.crt -noout -text \
  | grep -E "CA:TRUE|Key Usage"

# End-to-end TLS handshake against a running etcd node
openssl s_client -connect 192.168.1.10:2379 \
  -CAfile ca.crt -cert client.crt -key client.key </dev/null

6. etcd Flag Reference

# Server TLS
--cert-file=server.crt
--key-file=server.key
--trusted-ca-file=ca.crt
--client-cert-auth

# Peer TLS
--peer-cert-file=peer.crt
--peer-key-file=peer.key
--peer-trusted-ca-file=ca.crt
--peer-client-cert-auth

Notes

Why both -config and -extfile?

OpenSSL's extension handling is asymmetric across operations:

Operation Flag(s) needed Section read
req -x509 (self-signed CA) -config ca.ext [req] x509_extensions
req -new (CSR creation) -config <name>.ext [req] req_extensions
x509 -req (CA signing CSR) -extfile <name>.ext -extensions v3_req [v3_req]

Important: x509 -req ignores the extensions embedded in the CSR by default. You must re-supply them via -extfile/-extensions at signing time. This is both a footgun and a feature — it means the CA gets the final say on what extensions appear in the issued cert.

Serial number management

-CAcreateserial creates ca.srl on first use, then increments on each subsequent signing. The flag is idempotent — safe to leave on every command. For an audit trail in production, version-control ca.srl alongside the issued certs and inspect with cat ca.srl.

Key usage reference

Field CA server peer client
basicConstraints CA:TRUE (critical) CA:FALSE CA:FALSE CA:FALSE
keyCertSign
cRLSign
keyEncipherment
digitalSignature
serverAuth EKU
clientAuth EKU
SANs required
SKI / AKI SKI only both both both

Multi-node clusters

For a 3-node cluster, either:

Option A — shared peer cert (simpler): list all node IPs/DNS in [alt_names]

[alt_names]
DNS.1 = etcd-node-1
DNS.2 = etcd-node-2
DNS.3 = etcd-node-3
IP.1  = 127.0.0.1
IP.2  = 192.168.1.10
IP.3  = 192.168.1.11
IP.4  = 192.168.1.12

Option B — per-node peer cert (recommended for production): generate a separate peer-node-N.ext per node with only that node's IPs/DNS, sign each individually. Each node gets its own cert/key pair, so a compromised key only affects one node and rotation is per-node.

genpkey vs genrsa

genrsa genpkey
Scope RSA only RSA, EC, Ed25519, X25519, …
Output format PKCS#1 (BEGIN RSA PRIVATE KEY) PKCS#8 (BEGIN PRIVATE KEY)
Future-proof No Yes
pkey compatible Yes (reads both) Native

EC keys as an alternative

For new deployments, EC keys are smaller, faster, and have no downside in a private PKI. Drop-in replacement for the genpkey step:

# CA (use a stronger curve for the root)
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -out ca.key

# Leaves
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out server.key

All other commands are identical — pkey, req, and x509 are algorithm-agnostic.


Production Hardening

When moving from homelab to production, layer these on top of the workflow above:

Key protection

  • Keep ca.key offline. Sign new leaves from an air-gapped or HSM-backed workstation, never from the cluster nodes themselves.
  • Consider a two-tier PKI: an offline root CA that signs an online intermediate CA, which in turn signs leaves. Intermediate compromise → revoke and reissue from root without touching the root key.
  • File permissions: chmod 600 on every *.key, owned by the etcd user.

Lifetimes and rotation

  • Leaf cert lifetime ≤ 90 days with automated rotation is the modern norm. 365-day leaves are acceptable but require calendar discipline.
  • CA lifetime 5–10 years; plan the rollover before expiry, not after.
  • Document the rotation procedure as runbook before you need it.

Algorithm choices

  • RSA 4096 for CA, RSA 3072 minimum for leaves — or switch entirely to EC P-256/P-384 (smaller, faster, equivalent security at standard parameters).
  • Always -sha256 or -sha384 for signing. Reject SHA-1 anywhere.

Per-node certs, not shared

  • Each etcd node gets its own peer cert with only its own SANs.
  • Each client (kube-apiserver, etcdctl operator, backup tool) gets its own client cert with a distinct CN — etcd can authorize per-CN via RBAC.

Beyond raw OpenSSL For production at any meaningful scale, raw openssl becomes a liability. Consider:

  • cfssl (Cloudflare's tool) — JSON-driven, scriptable, well-suited to Kubernetes/etcd workflows.
  • smallstep step-ca — full ACME-capable internal CA, short-lived certs by default, good Kubernetes integration.
  • HashiCorp Vault PKI — dynamic cert issuance with revocation, audit log, fine-grained policy.

The OpenSSL flow in this document maps cleanly onto any of these — the same roles, SANs, EKUs, and key usages apply. The tool changes; the PKI design does not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment