Hands-On OpenSSL Lab: Generate, Inspect and Troubleshoot TLS Certificates with Real Commands

Hands-On OpenSSL Lab: Generate, Inspect and Troubleshoot TLS Certificates with Real Commands

📋 Key Takeaways
  • This hands-on lab teaches you to generate private keys and CSRs, add Subject Alternative Names correctly, self-sign and CA-sign certificates, inspect x509 details, and validate full certificate chains—all with copy-paste OpenSSL commands you can run locally in under an hour.
  • OpenSSL 3.x is the current baseline (1.1.1 reached end-of-life in September 2023, so verify you're not running it).
  • There are two paths to self-signing, and they differ subtly
  • This is where the lab gets interesting. Create your own root CA in four commands
11 min read · 2,098 words
Educational & Ethical Use Only — This article is provided for educational and ethical cybersecurity research purposes only. The techniques described should only be used on systems you own or have explicit permission to test. Always follow responsible disclosure and the laws applicable to you. Mitigations are included so engineers can harden real systems.
Security· 11 min read

TL;DR: What This OpenSSL Lab Teaches You

This hands-on lab teaches you to generate private keys and CSRs, add Subject Alternative Names correctly, self-sign and CA-sign certificates, inspect x509 details, and validate full certificate chains—all with copy-paste OpenSSL commands you can run locally in under an hour. Every exercise targets a failure mode you’ll hit in real production TLS deployments, from missing SANs to broken chain order.

Certificate management hasn’t gotten simpler—it’s gotten stricter. Since Google Chrome 58 dropped CN-only matching in favor of SAN validation, and CA/Browser Forum baseline requirements tightened around key usage and validity periods (398-day caps since September 2020), the old habit of generating a bare certificate and hoping for the best is dead. Whether you’re preparing for a PKI audit, deploying mTLS internally, or debugging why curl rejects your new endpoint, fluency in OpenSSL is non-negotiable. This lab builds that fluency—one exercise at a time.

Lab Setup: Installing OpenSSL and Creating a Workspace

OpenSSL 3.x is the current baseline (1.1.1 reached end-of-life in September 2023, so verify you’re not running it). Most Linux distributions ship with it; macOS bundles LibreSSL by default—which means some flags in this lab will misbehave. Install Homebrew’s OpenSSL if you’re on a Mac.

Verify your version first:

openssl version
# OpenSSL 3.2.1 30 Jan 2024 (Library: OpenSSL 3.2.1 30 Jan 2024)

Create an isolated workspace so you’re not scattering keys across your home directory:

mkdir -p ~/openssl-lab && cd ~/openssl-lab

When a command needs configuration beyond defaults—a CA signing operation, custom extensions—you’ll pass a config file with -config (or -extfile for extension-only files). You’ll see this in Exercises 3 and 5. Don’t skip it: relying on the system openssl.cnf silently drops your SANs during CA signing, which is the single most common mistake in this workflow.

Exercise 1: Generate a Private Key and CSR

How do you generate a private key and CSR with OpenSSL? Generate the key with genpkey (or genrsa/ecparam for specific algorithms), then feed it into openssl req -new.

Start with an RSA key—still the compatibility default:

openssl genrsa -out server.key 2048

Or ECDSA, which is smaller and faster for modern clients:

openssl ecparam -name prime256v1 -genkey -noout -out server-ec.key

Now the CSR. The interactive prompt asks for Common Name, organization, and so on—but for automation, use -subj:

openssl req -new -key server.key -out server.csr -subj "/C=US/ST=California/L=San Francisco/O=Lab Corp/CN=lab.example.com"

The CSR contains your public key and the distinguished name, signed with your private key to prove possession. It never contains the private key itself—the CSR is what you send to a CA, and the private key is what you never send anywhere.

Exercise 2: Add SANs (Subject Alternative Names) Correctly

Why does CN alone fail in modern browsers? Because every major browser—Chrome since v58 (April 2017), Firefox, Safari—validates hostname matches exclusively against the SAN extension. A certificate with only a CN matching the hostname gets rejected as a hostname mismatch, full stop.

Add SANs inline with -addext, available in OpenSSL 1.1.1+:

openssl req -new -key server.key -out server.csr 
  -subj "/C=US/O=Lab Corp/CN=lab.example.com" 
  -addext "subjectAltName=DNS:lab.example.com,DNS:www.lab.example.com,DNS:api.lab.example.com" 
  -addext "keyUsage=digitalSignature,keyEncipherment" 
  -addext "extendedKeyUsage=serverAuth"

For a multi-domain SAN configuration spanning wildcards and separate zones:

-addext "subjectAltName=DNS:*.lab.example.com,DNS:internal.example.net,IP:10.0.1.50"

Remember: SANs must be present in the issued certificate, not just the CSR. When a CA signs your CSR, extensions are typically copied from the CSR—but when you sign it yourself in Exercise 5, you control this explicitly. Miss this step and you’ll issue a technically valid certificate that every browser rejects.

Exercise 3: Self-Sign a Certificate and Set Validity

There are two paths to self-signing, and they differ subtly:

# Path A: one-shot, generates key + self-signed cert together
openssl req -x509 -newkey rsa:2048 -nodes -keyout self.key -out self.crt 
  -days 365 -subj "/CN=lab.example.com" 
  -addext "subjectAltName=DNS:lab.example.com" 
  -addext "basicConstraints=critical,CA:FALSE"
# Path B: sign an existing CSR directly
openssl x509 -req -in server.csr -signkey server.key -days 365 
  -extfile <(printf "subjectAltName=DNS:lab.example.comnkeyUsage=digitalSignaturenextendedKeyUsage=serverAuth")

The difference matters: openssl req -x509 builds the certificate from scratch, while openssl x509 -req processes a CSR and lets you inject extensions via -extfile. Without that extfile, x509 -req strips your SANs—this is the gotcha behind half the “my cert has no SAN” tickets.

Set -days to something sane. For labs, 90–365 days is typical; public CAs are capped at 398 days per the CA/Browser Forum ballot SC-22, with 47-day limits phasing in from 2026. Key usage and extended key usage extensions (serverAuth for TLS servers, clientAuth for mTLS clients) are what distinguish a working certificate from a doorstop.

Exercise 4: Inspect Certificates with x509 and req

How do you inspect a certificate’s details using OpenSSL? The workhorse is openssl x509 -text -noout:

openssl x509 -in self.crt -text -noout

Read the output deliberately:

  • Serial Number — must be unique per issuer; colliding serials break revocation
  • Issuer — the CA that signed it; equals Subject for self-signed certs
  • Validity — not-before and not-after dates; expired certs fail silently in some clients
  • Subject Alternative Name — the hostname list browsers actually check
  • X509v3 extensions — key usage, EKU, basic constraints

Verify a CSR’s integrity and fields before submitting it:

openssl req -verify -in server.csr -noout -text

If the signature fails here, the CSR was corrupted or tampered with—stop and regenerate. For quick field checks without dumping the full certificate:

openssl x509 -in self.crt -noout -subject -issuer -dates -serial

Exercise 5: Build a Mini CA and Sign a CSR

This is where the lab gets interesting. Create your own root CA in four commands:

# 1. CA private key
openssl genrsa -out ca.key 4096

# 2. CA certificate (note CA:TRUE)
openssl req -x509 -new -key ca.key -sha256 -days 1825 -out ca.crt 
  -subj "/C=US/O=Lab Root CA/CN=Lab Root CA" 
  -addext "basicConstraints=critical,CA:TRUE" 
  -addext "keyUsage=critical,keyCertSign,cRLSign"

# 3. Verify the leaf CSR
openssl req -verify -in server.csr -noout

# 4. Sign the leaf with SAN extensions
cat > leaf.ext <<EOF
basicConstraints=critical,CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=DNS:lab.example.com,DNS:www.lab.example.com
EOF

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key 
  -CAcreateserial -out server.crt -days 365 -sha256 -extfile leaf.ext

The -extfile is doing the heavy lifting—openssl x509 -req does not copy SANs from the CSR by default. If you sign without it, you issue a certificate that validates cryptographically but fails hostname matching everywhere.

Exercise 6: Validate the Certificate Chain

How do you verify a full certificate chain locally? With openssl verify and a correctly ordered CA file:

# Basic verification against your root
openssl verify -CAfile ca.crt server.crt
# server.crt: OK

Real deployments add an intermediate. Build the bundle and verify:

cat intermediate.crt ca.crt > ca-bundle.crt
openssl verify -CAfile ca.crt -untrusted intermediate.crt server.crt

Chain order in the served file is leaf first, intermediates in order, root last (though roots are optional in the served chain—clients already have them):

cat server.crt intermediate.crt > fullchain.pem

Mis-ordered chains are a top-three cause of TLS failures, and OpenSSL is stricter than browsers about it. This connects directly to the incomplete-chain errors you’ll debug in production—if you haven’t already, review our guide on TLS handshake failures for the protocol-level view.

Exercise 7: Troubleshoot Real-World TLS Errors

Why does a certificate fail validation and how do you fix it? Nearly every failure maps to one of these patterns:

  • unable to get local issuer certificate — the client can’t build the chain. Fix: serve the full chain (leaf + intermediates) or pass a complete bundle with -CAfile.
  • certificate has expired — check with openssl x509 -enddate -noout -in cert.pem. Fix: renew. Sysadmin truth: automated renewal (ACME/certbot) beats calendar reminders.
  • hostname mismatch — the SAN list doesn’t include the requested hostname. Fix: reissue with correct SANs.
  • self-signed certificate — expected for lab certs; fix by trusting your CA root (see Exercise 5) or using -CAfile / -k in curl for testing only.

The s_client subcommand is your remote debugger. Point it at any live endpoint:

openssl s_client -connect lab.example.com:443 -servername lab.example.com </dev/null

# Pull just the remote cert's expiry
openssl s_client -connect lab.example.com:443 -servername lab.example.com 2>/dev/null 
  | openssl x509 -noout -enddate

Read the Verify return code line at the bottom of the s_client output—it tells you exactly which validation stage failed. And note the -servername flag: without SNI, servers hosting multiple certs serve the wrong one, and you’ll debug a phantom mismatch.

For defenders, OWASP’s Transport Layer Protection Cheat Sheet remains the canonical reference for TLS configuration hardening, and CISA’s TLS guidance for federal agencies is a solid policy-level baseline.

Converting Formats: PEM, DER, PKCS#12

Different tools demand different encodings. PEM (Base64, -----BEGIN CERTIFICATE-----) is the Unix/web default; DER is binary (Windows tools, some Java); PKCS#12 bundles key + cert + chain in one password-protected file.

# PEM to DER
openssl x509 -in server.crt -outform DER -out server.der

# DER to PEM
openssl x509 -in server.der -inform DER -out server.pem

# Bundle key + cert + chain into PKCS#12 (for Windows/IIS imports)
openssl pkcs12 -export -out bundle.p12 
  -inkey server.key -in server.crt -certfile ca.crt

# Extract from PKCS#12 back to PEM
openssl pkcs12 -in bundle.p12 -out extracted.pem -nodes

Never email or commit a .p12—it contains the private key, and the export password is an inconvenience, not a control. For deeper key-handling hygiene, our primer on private key protection covers storage and rotation.

Cheat Sheet: Every Command in This Lab

# --- Keys ---
openssl genrsa -out server.key 2048
openssl ecparam -name prime256v1 -genkey -noout -out server-ec.key

# --- CSR ---
openssl req -new -key server.key -out server.csr -subj "/CN=lab.example.com"
openssl req -new -key server.key -out server.csr 
  -addext "subjectAltName=DNS:lab.example.com"

# --- Self-sign ---
openssl req -x509 -newkey rsa:2048 -nodes -keyout self.key -out self.crt 
  -days 365 -subj "/CN=lab.example.com"
openssl x509 -req -in server.csr -signkey server.key -days 365

# --- Inspect ---
openssl x509 -in cert.pem -text -noout
openssl x509 -in cert.pem -noout -subject -issuer -dates -serial
openssl req -verify -in server.csr -noout -text

# --- CA signing ---
openssl req -x509 -new -key ca.key -sha256 -days 1825 -out ca.crt 
  -addext "basicConstraints=critical,CA:TRUE"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key 
  -CAcreateserial -out server.crt -days 365 -extfile leaf.ext

# --- Chain validation ---
openssl verify -CAfile ca.crt server.crt
openssl verify -CAfile ca.crt -untrusted intermediate.crt server.crt

# --- Remote debugging ---
openssl s_client -connect host:443 -servername host
openssl s_client -connect host:443 2>/dev/null | openssl x509 -noout -enddate

# --- Format conversion ---
openssl x509 -in cert.crt -outform DER -out cert.der
openssl pkcs12 -export -out bundle.p12 -inkey server.key -in server.crt

Frequently Asked Questions

How do I generate a CSR with a SAN in one OpenSSL command?

Use openssl req -new -key server.key -out server.csr -subj "/CN=example.com" -addext "subjectAltName=DNS:example.com,DNS:www.example.com". The -addext flag (OpenSSL 1.1.1+) injects extensions directly, no config file required.

How do I check if a certificate matches its private key?

Compare the public key hashes:

openssl x509 -noout -pubkey -in cert.pem | openssl sha256
openssl pkey -pubout -in key.pem | openssl sha256

If the hashes match, the key pairs with the cert. On RSA keys, you can also compare modulus via openssl x509 -noout -modulus vs openssl rsa -noout -modulus.

Why does OpenSSL say ‘unable to get local issuer certificate’?

The chain is incomplete: either the server isn’t serving intermediates, or your -CAfile bundle is missing the signing CA. Fix by appending intermediates to the served certificate file or providing a complete bundle to openssl verify / s_client -CAfile.

How do I view a certificate’s expiration date with OpenSSL?

Locally: openssl x509 -enddate -noout -in cert.pem. For remote endpoints: openssl s_client -connect host:443 2>/dev/null | openssl x509 -noout -enddate.

Is a self-signed certificate safe to use?

For labs, internal testing, and localhost development—absolutely, provided you control trust distribution. For public-facing production services, no: browsers will warn every visitor, and users conditioned to click through warnings are a phishing risk multiplier. Use a real CA (Let’s Encrypt is free) for anything public.

Hmmnm
Published by Hmmnm

Hands-on cybersecurity tutorials, CVE breakdowns, and guided learning paths — written and lab-tested by the Hmmnm team.

🛡️ Hmmnm also delivers this expertise as a service — security testing, assessment & training.
Keep going — the structured way
This post is one step. The learning paths chain the next ones for you, with progress tracking and no account needed.
Follow a learning path →

Prabhu Kalyan Samal

Application Security Consultant at TCS. Certifications: CompTIA SecurityX, Burp Suite Certified Practitioner, Azure Security Engineer, Azure AI Engineer, Certified Red Team Operator, eWPTX v3, LPT, CompTIA PenTest+, Professional Cloud Security Engineer, SC-900, SC-200, PSPO I, CEH, Oracle Java SE 8, ISP, Six Sigma Green Belt, DELF, AutoCAD. Writing about ethical hacking, security tutorials, and tech education at Hmmnm.