Those browser warnings for self-signed certificates become a real productivity killer in development environments. While Let's Encrypt works great for public-facing sites, internal tools and staging environments often need a more flexible solution where you control the root of trust.
Before we dive into OpenSSL commands, let's understand the moving parts:
Root CA Certificate (the ultimate trust anchor) Intermediate CA (optional but recommended) Server Certificates (end-entity certs) Certificate Revocation List (CRL)
First, create a configuration file (root-ca.cnf):
[ req ] default_bits = 4096 default_md = sha256 distinguished_name = req_distinguished_name x509_extensions = v3_ca [ req_distinguished_name ] countryName = Country Name (2 letter code) stateOrProvinceName = State or Province Name localityName = Locality Name 0.organizationName = Organization Name organizationalUnitName = Organizational Unit Name commonName = Common Name emailAddress = Email Address [ v3_ca ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:true keyUsage = critical, digitalSignature, cRLSign, keyCertSign
Generate the root key and certificate:
openssl genrsa -aes256 -out root-ca.key 4096 openssl req -x509 -new -nodes -key root-ca.key -sha256 -days 3650 \ -out root-ca.crt -config root-ca.cnf
For better security hygiene, never issue end-entity certificates directly from your root CA. Create an intermediate:
openssl genrsa -aes256 -out intermediate-ca.key 4096 openssl req -new -key intermediate-ca.key -out intermediate-ca.csr \ -config intermediate-ca.cnf openssl x509 -req -in intermediate-ca.csr -CA root-ca.crt \ -CAkey root-ca.key -CAcreateserial -out intermediate-ca.crt \ -days 1825 -sha256 -extfile intermediate-ca.ext
Now let's create a certificate for internal-service.example.com:
openssl genrsa -out internal-service.key 2048 openssl req -new -key internal-service.key -out internal-service.csr \ -config csr.cnf openssl x509 -req -in internal-service.csr -CA intermediate-ca.crt \ -CAkey intermediate-ca.key -CAcreateserial -out internal-service.crt \ -days 365 -sha256 -extfile server-cert.ext
For Windows systems (PowerShell):
Import-Certificate -FilePath root-ca.crt -CertStoreLocation Cert:\LocalMachine\Root
For Linux (Debian/Ubuntu):
sudo cp root-ca.crt /usr/local/share/ca-certificates/ sudo update-ca-certificates
For Chrome/Chromium browsers:
# On Linux cp root-ca.crt ~/.pki/nssdb/ certutil -d sql:~/.pki/nssdb -A -t "C,," -n "MyCompany CA" -i root-ca.crt # On Windows certutil -addstore -f "ROOT" root-ca.crt
Consider using configuration management tools:
# Ansible example - name: Install root CA certificate win_certificate_store: file_path: "{{ root_ca_path }}" store_location: LocalMachine store_name: Root state: present
html
Building an Enterprise PKI: Step-by-Step Guide to Creating Your Own Root CA for Internal HTTPS
When managing internal services across an organization, browser security warnings for self-signed certificates become a constant nuisance. A properly configured private CA lets you:
- Issue trusted certificates for internal domains (dev.example.com, vpn.corp.local)
- Automate certificate lifecycle management
- Implement certificate revocation through CRL/OCSP
- Maintain consistent security policies across all services
For a production-ready CA, you'll need:
# Infrastructure Requirements - Dedicated Linux server (physical preferred) - Hardware Security Module (HSM) for key storage - Backup procedures for CA keys - Network segregation from production
Generate the root CA key with maximum security (4096-bit RSA with SHA-256):
openssl genrsa -aes256 -out private/ca.key.pem 4096 chmod 400 private/ca.key.pem # Create root certificate openssl req -config openssl.cnf \ -key private/ca.key.pem \ -new -x509 -days 7300 -sha256 -extensions v3_ca \ -out certs/ca.cert.pem
Never issue directly from the root. Create an intermediate CA:
# Generate intermediate key openssl genrsa -aes256 \ -out intermediate/private/intermediate.key.pem 4096 # Create CSR openssl req -config intermediate/openssl.cnf -new -sha256 \ -key intermediate/private/intermediate.key.pem \ -out intermediate/csr/intermediate.csr.pem # Sign with root CA openssl ca -config openssl.cnf -extensions v3_intermediate_ca \ -days 3650 -notext -md sha256 \ -in intermediate/csr/intermediate.csr.pem \ -out intermediate/certs/intermediate.cert.pem
Define certificate constraints in openssl.cnf:
[ server_cert ] basicConstraints = CA:FALSE nsCertType = server nsComment = "Enterprise Internal Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer keyUsage = digitalSignature, keyEncipherment extendedKeyUsage = serverAuth
Distribute your root certificate through:
- Group Policy (Windows)
- MDM profiles (macOS/iOS)
- Puppet/Ansible (Linux)
- Chrome Enterprise policies
Example using certbot with your CA:
certbot certonly --manual \ --preferred-challenges dns \ --manual-auth-hook ./authenticator.sh \ --manual-cleanup-hook ./cleanup.sh \ -d *.dev.example.com \ --cert-path /etc/ssl/private/ \ --key-path /etc/ssl/private/
Critical operational considerations:
# Check certificate expiration openssl x509 -noout -dates -in cert.pem # Generate CRL openssl ca -config openssl.cnf \ -gencrl -out crl/root-ca.crl.pem # OCSP responder setup openssl ocsp -index index.txt \ -port 2560 -text -sha256 \ -CA certs/ca-chain.cert.pem \ -rkey private/ocsp.key.pem \ -rsigner certs/ocsp.cert.pem
- Store root CA keys offline (air-gapped system)
- Implement certificate transparency logging
- Rotate intermediate CA keys annually
- Audit certificate usage monthly