Full walkthrough for generating a CA, server, peer, and client certificates for
etcd using the modern genpkey/pkey subcommands and .ext config files.
| 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 |
[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(notreq_extensions) is the right key whenopenssl reqis invoked with-x509to produce a self-signed cert.prompt = nomakes OpenSSL read DN fields directly from this section instead of prompting.
# 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 -nooutUsed 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# 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 -nooutUsed 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# 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 -nooutUsed 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# 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# 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# 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
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.
-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.
| 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 |
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.12Option 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.
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 |
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.keyAll other commands are identical — pkey, req, and x509 are
algorithm-agnostic.
When moving from homelab to production, layer these on top of the workflow above:
Key protection
- Keep
ca.keyoffline. 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 600on 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
-sha256or-sha384for 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.