PKI Certificate Authority Setup Guide: Building Your Internal CA Infrastructure
A Private PKI (Public Key Infrastructure) gives your organization complete control over certificate issuance. Whether you're implementing mTLS for microservices, client certificate authentication, internal HTTPS, or code signing, an internal CA provides flexibility that public CAs cannot match—custom validity periods, proprietary extensions, certificates for internal domains, and no per-certificate costs.
This guide covers designing and deploying enterprise PKI infrastructure, from architecture decisions through implementation with HashiCorp Vault, Active Directory Certificate Services, step-ca, and cloud-based solutions.
When You Need a Private PKI
Use a private PKI when:
- Issuing certificates for internal services not accessible from the internet
- Implementing mTLS (mutual TLS) between microservices
- Requiring client certificate authentication
- Needing short-lived certificates (hours or days) for ephemeral workloads
- Signing internal code and scripts
- Controlling certificate issuance policies completely
- Avoiding per-certificate costs at scale
Use a public CA when:
- Certificates must be trusted by external users/browsers
- You need certificates for public-facing websites
- Regulatory requirements mandate publicly-audited CAs
Most organizations use both—public CAs for external services, private PKI for internal infrastructure.
PKI Architecture Fundamentals
Two-Tier vs Three-Tier Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ PKI Architecture Options │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ TWO-TIER (Recommended for most) THREE-TIER (Large enterprise) │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ ROOT CA │ │ ROOT CA │ │
│ │ (Offline) │ │ (Offline) │ │
│ │ 20-year cert │ │ 20-year cert │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ │ Signs │ Signs │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ ISSUING CA │ │ POLICY CA │ │
│ │ (Online) │ │ (Offline) │ │
│ │ 5-year cert │ │ 10-year cert │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ │ Issues │ Signs │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ End-Entity │ │ ISSUING CA(s) │ │
│ │ Certificates │ │ (Online) │ │
│ │ (days to years) │ │ 5-year cert │ │
│ └─────────────────┘ └────────┬────────┘ │
│ │ │
│ │ Issues │
│ ▼ │
│ ┌─────────────────┐ │
│ │ End-Entity │ │
│ │ Certificates │ │
│ └─────────────────┘ │
│ │
│ Pros: Pros: │
│ • Simpler to manage • Policy separation │
│ • Fewer components • Multiple issuing CAs │
│ • Sufficient security • Regulatory compliance │
│ │
│ Cons: Cons: │
│ • Less flexibility • More complex │
│ • Single issuing CA • More infrastructure │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Choose two-tier for most organizations—it provides proper security isolation while remaining manageable.
Choose three-tier when you need:
- Multiple issuing CAs with different policies (internal vs. partner vs. IoT)
- Regulatory requirements (government, financial services)
- Geographic distribution with regional issuing CAs
Certificate Validity Planning
| CA Level | Recommended Validity | Key Algorithm | Key Storage |
|---|---|---|---|
| Root CA | 15-20 years | RSA 4096 or ECDSA P-384 | HSM (offline) |
| Policy CA | 8-10 years | RSA 4096 or ECDSA P-384 | HSM (offline) |
| Issuing CA | 3-5 years | RSA 2048+ or ECDSA P-256 | HSM (online) or software |
| End-Entity | Hours to 2 years | RSA 2048 or ECDSA P-256 | Software |
Key validity rule: Each CA's certificate should be at least 2x the validity of certificates it issues, to allow for proper renewal cycles.
Root CA Setup
The Root CA is the trust anchor of your entire PKI. Its security is paramount—if the root key is compromised, your entire PKI must be rebuilt.
Offline Root CA Requirements
Hardware:
- Dedicated, air-gapped machine (never connected to network)
- Hardware Security Module (HSM) for key storage
- Secure boot enabled, encrypted disk
- Physical access controls (locked room, surveillance)
Software:
- Minimal OS installation (no unnecessary packages)
- Only CA software installed
- No SSH, no remote access capability
Root CA Generation with OpenSSL
Create the root CA configuration:
# Create directory structure
mkdir -p /root/ca/{certs,crl,newcerts,private,csr}
chmod 700 /root/ca/private
touch /root/ca/index.txt
echo 1000 > /root/ca/serial
echo 1000 > /root/ca/crlnumber
# Create OpenSSL configuration
cat > /root/ca/openssl.cnf << 'EOF'
[ca]
default_ca = CA_default
[CA_default]
dir = /root/ca
certs = $dir/certs
crl_dir = $dir/crl
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/certs/root-ca.crt
serial = $dir/serial
crlnumber = $dir/crlnumber
crl = $dir/crl/root-ca.crl
private_key = $dir/private/root-ca.key
default_md = sha256
default_days = 7300
default_crl_days = 365
policy = policy_strict
x509_extensions = v3_intermediate_ca
[policy_strict]
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[req]
default_bits = 4096
distinguished_name = req_distinguished_name
string_mask = utf8only
default_md = sha256
x509_extensions = v3_ca
[req_distinguished_name]
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
commonName = Common Name
[v3_ca]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[v3_intermediate_ca]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
EOF
Generate the root CA key and certificate:
# Generate root private key (RSA 4096)
openssl genrsa -aes256 -out /root/ca/private/root-ca.key 4096
chmod 400 /root/ca/private/root-ca.key
# Generate root certificate (20-year validity)
openssl req -config /root/ca/openssl.cnf \
-key /root/ca/private/root-ca.key \
-new -x509 -days 7300 -sha256 \
-extensions v3_ca \
-out /root/ca/certs/root-ca.crt \
-subj "/C=US/ST=California/L=San Francisco/O=Example Corp/OU=IT Security/CN=Example Corp Root CA"
# Verify the certificate
openssl x509 -noout -text -in /root/ca/certs/root-ca.crt
Root CA with ECDSA
For modern deployments, ECDSA provides equivalent security with smaller keys:
# Generate ECDSA root key (P-384 curve)
openssl ecparam -genkey -name secp384r1 | \
openssl ec -aes256 -out /root/ca/private/root-ca.key
# Generate root certificate
openssl req -config /root/ca/openssl.cnf \
-key /root/ca/private/root-ca.key \
-new -x509 -days 7300 -sha384 \
-extensions v3_ca \
-out /root/ca/certs/root-ca.crt \
-subj "/C=US/ST=California/L=San Francisco/O=Example Corp/CN=Example Corp Root CA"
Issuing CA Setup
The Issuing CA handles day-to-day certificate operations. It runs online and can be automated.
Generate Issuing CA CSR
On your issuing CA server:
# Create issuing CA directory structure
mkdir -p /etc/pki/issuing-ca/{certs,crl,newcerts,private,csr}
chmod 700 /etc/pki/issuing-ca/private
# Generate issuing CA private key
openssl genrsa -aes256 -out /etc/pki/issuing-ca/private/issuing-ca.key 2048
chmod 400 /etc/pki/issuing-ca/private/issuing-ca.key
# Generate CSR for the issuing CA
openssl req -new -sha256 \
-key /etc/pki/issuing-ca/private/issuing-ca.key \
-out /etc/pki/issuing-ca/csr/issuing-ca.csr \
-subj "/C=US/ST=California/L=San Francisco/O=Example Corp/OU=IT Security/CN=Example Corp Issuing CA"
Sign Issuing CA with Root CA
Transfer the CSR to the offline root CA and sign it:
# On the root CA machine
openssl ca -config /root/ca/openssl.cnf \
-extensions v3_intermediate_ca \
-days 1825 -notext -md sha256 \
-in /root/ca/csr/issuing-ca.csr \
-out /root/ca/certs/issuing-ca.crt
# Verify the certificate chain
openssl verify -CAfile /root/ca/certs/root-ca.crt \
/root/ca/certs/issuing-ca.crt
Create Certificate Chain
# Create the chain file (issuing + root)
cat /etc/pki/issuing-ca/certs/issuing-ca.crt \
/etc/pki/issuing-ca/certs/root-ca.crt > \
/etc/pki/issuing-ca/certs/ca-chain.crt
Implementation Options
Option 1: HashiCorp Vault PKI Engine
Vault is excellent for automated, API-driven certificate issuance.
# Enable PKI secrets engine for root CA
vault secrets enable -path=pki pki
# Configure max lease (20 years for root)
vault secrets tune -max-lease-ttl=175200h pki
# Generate root certificate
vault write pki/root/generate/internal \
common_name="Example Corp Root CA" \
ttl=175200h \
key_type="rsa" \
key_bits=4096
# Enable PKI for issuing CA
vault secrets enable -path=pki_int pki
# Configure max lease (5 years)
vault secrets tune -max-lease-ttl=43800h pki_int
# Generate intermediate CSR
vault write -format=json pki_int/intermediate/generate/internal \
common_name="Example Corp Issuing CA" \
key_type="rsa" \
key_bits=2048 \
| jq -r '.data.csr' > issuing-ca.csr
# Sign with root CA
vault write -format=json pki/root/sign-intermediate \
[email protected] \
format=pem_bundle \
ttl=43800h \
| jq -r '.data.certificate' > issuing-ca.crt
# Set signed certificate
vault write pki_int/intermediate/set-signed [email protected]
Configure certificate roles:
# Create role for web server certificates
vault write pki_int/roles/webserver \
allowed_domains="example.com,internal.example.com" \
allow_subdomains=true \
max_ttl=8760h \
key_type="rsa" \
key_bits=2048 \
key_usage="DigitalSignature,KeyEncipherment" \
ext_key_usage="ServerAuth"
# Create role for client certificates (mTLS)
vault write pki_int/roles/client-auth \
allowed_domains="example.com" \
allow_any_name=true \
max_ttl=720h \
key_type="ec" \
key_bits=256 \
key_usage="DigitalSignature" \
ext_key_usage="ClientAuth"
# Issue a certificate
vault write pki_int/issue/webserver \
common_name="api.internal.example.com" \
ttl=720h
Option 2: Active Directory Certificate Services (ADCS)
For Windows-centric environments, ADCS integrates deeply with Active Directory.
Installation (PowerShell):
# Install ADCS role
Install-WindowsFeature -Name AD-Certificate, ADCS-Cert-Authority -IncludeManagementTools
# Configure as Enterprise Root CA (for single-tier) or Subordinate CA
Install-AdcsCertificationAuthority `
-CAType EnterpriseSubordinateCA `
-CACommonName "Example Corp Issuing CA" `
-KeyLength 2048 `
-HashAlgorithmName SHA256 `
-CryptoProviderName "RSA#Microsoft Software Key Storage Provider" `
-ValidityPeriod Years `
-ValidityPeriodUnits 5
# Install OCSP responder
Install-WindowsFeature -Name ADCS-Online-Cert
Add-WindowsFeature ADCS-Online-Cert
Install-AdcsOnlineResponder
Create certificate templates:
# Duplicate Web Server template for custom configuration
$ConfigContext = ([ADSI]"LDAP://RootDSE").configurationNamingContext
$TemplatePath = "CN=Certificate Templates,CN=Public Key Services,CN=Services,$ConfigContext"
# Use Certificate Templates MMC or PowerShell PSPKI module
# Install-Module PSPKI
Import-Module PSPKI
Get-CertificateTemplate -Name "WebServer" | `
Get-CertificateTemplateAcl | `
Add-CertificateTemplateAcl -Identity "Domain Computers" -AccessType Allow -AccessMask Enroll
Option 3: step-ca (Open Source)
step-ca is a lightweight, modern CA perfect for DevOps environments.
# Install step CLI and step-ca
wget https://dl.step.sm/gh-release/certificates/latest/step-ca_linux_amd64.tar.gz
tar -xf step-ca_linux_amd64.tar.gz
sudo mv step-ca /usr/local/bin/
# Initialize CA
step ca init \
--name="Example Corp CA" \
--dns="ca.example.com" \
--address=":8443" \
--provisioner="admin"
# Start the CA
step-ca $(step path)/config/ca.json
Configure ACME provisioner for automated certificate issuance:
# Add ACME provisioner
step ca provisioner add acme --type ACME
# Configure certificate templates
cat > /etc/step/templates/x509/server.tpl << 'EOF'
{
"subject": {{ toJson .Subject }},
"sans": {{ toJson .SANs }},
"keyUsage": ["digitalSignature", "keyEncipherment"],
"extKeyUsage": ["serverAuth"]
}
EOF
Option 4: Cloud-Based Private CAs
AWS Private CA:
# Create private CA
aws acm-pca create-certificate-authority \
--certificate-authority-configuration \
"KeyAlgorithm=RSA_2048,SigningAlgorithm=SHA256WITHRSA,Subject={CommonName=Example Corp Issuing CA,Organization=Example Corp,Country=US}" \
--certificate-authority-type SUBORDINATE \
--tags Key=Environment,Value=Production
# Get CSR for signing by external root
aws acm-pca get-certificate-authority-csr \
--certificate-authority-arn arn:aws:acm-pca:us-east-1:123456789:certificate-authority/xxx \
--output text > issuing-ca.csr
Google Cloud Certificate Authority Service:
# Create CA pool
gcloud privateca pools create example-ca-pool \
--location=us-central1 \
--tier=enterprise
# Create subordinate CA
gcloud privateca subordinates create example-issuing-ca \
--pool=example-ca-pool \
--location=us-central1 \
--subject="CN=Example Corp Issuing CA,O=Example Corp,C=US" \
--key-algorithm=rsa-pkcs1-2048-sha256 \
--max-chain-length=0
CRL and OCSP Configuration
CRL Distribution Points
# Generate CRL (on issuing CA)
openssl ca -config /etc/pki/issuing-ca/openssl.cnf \
-gencrl -out /etc/pki/issuing-ca/crl/issuing-ca.crl
# Convert to DER format (required by some clients)
openssl crl -in /etc/pki/issuing-ca/crl/issuing-ca.crl \
-outform DER -out /etc/pki/issuing-ca/crl/issuing-ca.crl.der
# Host CRL via HTTP (must be highly available)
# Configure web server to serve: http://crl.example.com/issuing-ca.crl
OCSP Responder Setup
# Generate OCSP signing certificate
openssl req -new -nodes \
-keyout /etc/pki/ocsp/ocsp.key \
-out /etc/pki/ocsp/ocsp.csr \
-subj "/CN=OCSP Responder/O=Example Corp"
# Sign with issuing CA (add OCSP signing EKU)
openssl ca -config /etc/pki/issuing-ca/openssl.cnf \
-extensions ocsp \
-in /etc/pki/ocsp/ocsp.csr \
-out /etc/pki/ocsp/ocsp.crt
# Start OCSP responder
openssl ocsp -port 8080 \
-index /etc/pki/issuing-ca/index.txt \
-CA /etc/pki/issuing-ca/certs/ca-chain.crt \
-rkey /etc/pki/ocsp/ocsp.key \
-rsigner /etc/pki/ocsp/ocsp.crt \
-text
For production, use a dedicated OCSP responder like:
- Boulder (Let's Encrypt's CA software)
- EJBCA (Enterprise Java-based CA)
- AWS Private CA (built-in OCSP)
Certificate Templates
Web Server Template
# OpenSSL extension configuration
[webserver]
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
crlDistributionPoints = URI:http://crl.example.com/issuing-ca.crl
authorityInfoAccess = OCSP;URI:http://ocsp.example.com
Client Authentication Template (mTLS)
[client_auth]
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = clientAuth
crlDistributionPoints = URI:http://crl.example.com/issuing-ca.crl
Code Signing Template
[code_signing]
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = codeSigning
crlDistributionPoints = URI:http://crl.example.com/issuing-ca.crl
Distributing Root CA Trust
Windows (Group Policy)
# Import root certificate to GPO
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("C:\certs\root-ca.crt")
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Root", "LocalMachine")
$store.Open("ReadWrite")
$store.Add($cert)
$store.Close()
Linux
# RHEL/CentOS
cp root-ca.crt /etc/pki/ca-trust/source/anchors/
update-ca-trust
# Debian/Ubuntu
cp root-ca.crt /usr/local/share/ca-certificates/
update-ca-certificates
# Verify
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt test-cert.crt
macOS
# Add to system keychain
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain root-ca.crt
Kubernetes
apiVersion: v1
kind: ConfigMap
metadata:
name: ca-certificates
data:
root-ca.crt: |
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIUB...
-----END CERTIFICATE-----
---
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
volumeMounts:
- name: ca-certs
mountPath: /etc/ssl/certs/root-ca.crt
subPath: root-ca.crt
volumes:
- name: ca-certs
configMap:
name: ca-certificates
Monitoring and Auditing
Key Metrics to Monitor
┌─────────────────────────────────────────────────────────────────────────┐
│ PKI Monitoring Dashboard │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ CA HEALTH CERTIFICATE METRICS │
│ ────────────────────────────────────────────────────────────────────── │
│ │
│ □ CA certificate expiry □ Certificates issued (daily/weekly) │
│ (alert at 1 year, 6mo, □ Certificates expiring soon │
│ 3mo, 1mo) □ Certificates revoked │
│ │
│ □ CRL last published □ Failed issuance attempts │
│ (alert if > 24h old) □ Invalid CSR submissions │
│ │
│ □ OCSP responder uptime □ Average certificate lifetime │
│ (must be highly available) □ Certificates by template type │
│ │
│ SECURITY AUDITING COMPLIANCE │
│ ────────────────────────────────────────────────────────────────────── │
│ │
│ □ All key access logged □ Certificate inventory complete │
│ □ Admin actions audited □ Key ceremony documentation │
│ □ Failed auth attempts □ Policy compliance (key sizes, algos) │
│ □ Certificate downloads □ Backup verification schedule │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Prometheus Metrics (for Vault PKI)
# prometheus-rules.yaml
groups:
- name: pki-alerts
rules:
- alert: CACertificateExpiring
expr: (vault_pki_cert_expiry_seconds - time()) < 31536000
for: 1h
labels:
severity: warning
annotations:
summary: "CA certificate expiring within 1 year"
- alert: CRLStale
expr: (time() - vault_pki_crl_last_updated) > 86400
for: 1h
labels:
severity: critical
annotations:
summary: "CRL not updated in 24 hours"
Common Pitfalls
| Pitfall | Impact | Prevention |
|---|---|---|
| Root CA key on network-connected system | Complete PKI compromise | Offline, air-gapped root CA with HSM |
| Missing CRL/OCSP distribution | Clients can't verify revocation | Configure and monitor distribution points |
| Single issuing CA | Single point of failure | Consider multiple issuing CAs for resilience |
| No key backup | Unrecoverable key loss | Encrypted backups in separate locations |
| Short root CA validity | Frequent trust store updates | 15-20 year root certificates |
| Weak key algorithms | Future cryptographic breaks | RSA 2048+ or ECDSA P-256+ |
| No monitoring | Undetected expiration/compromise | Comprehensive monitoring and alerting |
| Incomplete certificate chains | Client connection failures | Always serve full chain |
| No audit logging | Compliance violations, breach investigation | Log all CA operations |
| Single-person key access | Insider threat, key compromise | Multi-person authorization for root key |
Conclusion
Building a private PKI requires careful architecture planning, robust security controls for root CA protection, and automation for issuing CA operations. Key decisions include choosing between two-tier and three-tier architectures, selecting key algorithms and validity periods, and implementing proper CRL/OCSP infrastructure.
Key takeaways:
- Keep your root CA offline with HSM-protected keys and multi-person authorization
- Use two-tier architecture unless regulatory requirements mandate three-tier
- Implement both CRL and OCSP for complete revocation coverage
- Automate certificate issuance with Vault, step-ca, or ADCS
- Monitor CA health and certificate expiration continuously
- Document key ceremonies and maintain audit trails for compliance
For related topics, see our guides on HSM Certificate Storage and Certificate Lifecycle Management.