Building a local Certificate Authority gives you complete control over certificate issuance for your development environment: you define the validity period, the subject details, the Subject Alternative Names, the key usage extensions, and the expiry. Every certificate you issue is trusted by any browser or client configured to trust your CA root. You can issue certificates for any internal hostname, IP address, or wildcard pattern without being constrained by Let’s Encrypt’s domain validation requirements or mkcert’s defaults.
This guide covers two scenarios: quick local CA setup using mkcert (the right choice for individual developers needing localhost HTTPS), and a full manual OpenSSL CA (the right choice for teams, microservice environments, container deployments, mTLS setups, and anyone who wants to understand and control the CA infrastructure rather than use a black box).
mkcert vs Manual OpenSSL CA: Choose the Right Approach
| Scenario | Use mkcert | Use manual OpenSSL CA |
| Single developer, localhost only | Yes, mkcert is faster and simpler | Unnecessary overhead |
| Team sharing a CA across multiple machines | Possible but awkward | Better: share the CA root cert across the team |
| Docker/Kubernetes containers needing HTTPS | Possible with root cert injection | Better: full control over CA and cert generation |
| Microservices with mTLS (mutual TLS) | Not designed for this | Yes: issue client and server certificates from same CA |
| Testing certificate chain validation, pinning, or expiry behavior | Limited | Yes: full control over all certificate properties |
| Understanding what a CA does internally | No (mkcert hides the details) | Yes: best way to learn PKI mechanics |
| Windows-first development environment | mkcert handles Windows trust store automatically | Requires manual import into Windows trust store |
Part 1: mkcert (Quick Path for Individual Developers)
mkcert is an open-source tool by Filippo Valsorda that creates a locally-trusted CA and manages certificate issuance. It automatically installs the CA root into all major trust stores on your machine: the OS trust store (used by Chrome and most system tools), Firefox’s NSS database, and the Java trust store if applicable.
| # Install mkcert:
# macOS (Homebrew):   brew install mkcert # Linux:              apt install mkcert OR brew install mkcert # Windows (Chocolatey): choco install mkcert # Windows (Scoop):     scoop install mkcert
# Install the local CA root into your system trust stores: $ mkcert -install # This creates the CA root key and cert in your mkcert home directory # and installs the cert into browsers and OS trust stores.
# Generate a certificate for localhost and common dev hostnames: $ mkcert localhost 127.0.0.1 ::1 myapp.local # Produces: localhost+3.pem and localhost+3-key.pem # Use these in your dev server configuration.
# Find where mkcert stores the CA root (important: keep this secure): $ mkcert -CAROOT # The rootCA.pem and rootCA-key.pem files are here. # NEVER commit rootCA-key.pem to version control. |
The mkcert CA root key (rootCA-key.pem) can sign any certificate for any domain. Anyone who obtains this file can create certificates trusted by your browser. Keep it out of version control and do not distribute it. If you need team members to trust the same CA, distribute only rootCA.pem (the public certificate), not the key.
Part 2: Manual OpenSSL CA (Full Control)
Building a CA manually with OpenSSL gives you complete understanding and control of every step. The process has three phases: creating the CA root, creating a CA configuration file that controls how certificates are signed, and issuing certificates.
Step 1: Create the CA directory structure
| # Create a dedicated directory for your local CA:
$ mkdir -p ~/localCA/{certs,crl,newcerts,private} $ cd ~/localCA $ touch index.txt $ echo 1000 > serial
# index.txt: database of issued certificates # serial: current certificate serial number (increments with each cert) # private/: store the CA private key here (restricted permissions) $ chmod 700 private |
Step 2: Create the CA configuration file
The OpenSSL configuration file controls how the CA signs certificates. The most important section for development use is the extension that adds SAN entries to signed certificates. Without explicit SAN configuration, browsers reject the certificates.
| # Create ~/localCA/openssl.cnf:
# (Save this file before running any signing commands)
[ ca ] default_ca = CA_default
[ CA_default ] dir              = /Users/yourname/localCA # CHANGE THIS to your actual path certs            = $dir/certs crl_dir          = $dir/crl new_certs_dir    = $dir/newcerts database         = $dir/index.txt serial           = $dir/serial private_key      = $dir/private/ca.key certificate      = $dir/certs/ca.crt default_md       = sha256 default_days     = 825 preserve         = no policy           = policy_loose copy_extensions  = copy # CRITICAL: copies SANs from CSR to signed cert
[ policy_loose ] countryName            = optional stateOrProvinceName    = optional localityName           = optional organizationName       = optional organizationalUnitName = optional commonName             = supplied emailAddress           = optional
[ req ] default_bits       = 2048 default_md         = sha256 distinguished_name = req_distinguished_name x509_extensions    = v3_ca
[ req_distinguished_name ] commonName = Common Name commonName_default = Local Development CA
[ v3_ca ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:true keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ v3_server ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer basicConstraints = CA:false keyUsage = critical, digitalSignature, keyEncipherment extendedKeyUsage = serverAuth |
The copy_extensions = copy line in the CA_default section is the most important setting for modern certificate compatibility. Without it, Subject Alternative Name entries from the Certificate Signing Request (CSR) are stripped when the CA signs the certificate. The result is a certificate with a correct CN but no SAN entries, which all modern browsers reject. This is the most common reason manual CA certificates fail in browsers despite appearing to be correctly configured.
Step 3: Generate the CA root key and self-signed certificate
| # Generate the CA private key:
$ openssl genrsa -aes256 -out ~/localCA/private/ca.key 4096 # -aes256 encrypts the key with a passphrase. Remember this passphrase. # You will need it every time you sign a certificate.
# Create the CA root certificate (self-signed, 10-year validity): $ openssl req -config ~/localCA/openssl.cnf \ -key ~/localCA/private/ca.key \ -new -x509 -days 3650 \ -extensions v3_ca \ -out ~/localCA/certs/ca.crt
# When prompted for Subject details: # Common Name: Local Development CAÂ (or your team name) # Other fields: can be left blank or filled as desired
# Verify the CA certificate: $ openssl x509 -in ~/localCA/certs/ca.crt -noout -text | head -30 # Check that: CA:TRUE is in Basic Constraints #Â Â Â Â Â Â Â Â Â Â Â Â keyCertSign is in Key Usage #Â Â Â Â Â Â Â Â Â Â Â Â Validity is 10 years as expected |
Step 4: Generate and sign a server certificate
For each service or hostname that needs HTTPS, generate a key pair and a Certificate Signing Request (CSR), then sign it with your local CA.
| # 1. Generate a private key for the server certificate:
$ openssl genrsa -out myapp.local.key 2048
# 2. Create a CSR with SAN extensions: $ openssl req -new -key myapp.local.key \ -subj ‘/CN=myapp.local’ \ -addext ‘subjectAltName=DNS:myapp.local,DNS:*.myapp.local,IP:127.0.0.1’ \ -out myapp.local.csr
# -addext flag (OpenSSL 1.1.1+) embeds the SAN in the CSR. # The CA config with copy_extensions=copy then copies this into the cert.
# 3. Sign the CSR with your local CA: $ openssl ca -config ~/localCA/openssl.cnf \ -extensions v3_server \ -days 825 \ -notext \ -in myapp.local.csr \ -out myapp.local.crt
# 4. Verify the resulting certificate has the correct SAN entries: $ openssl x509 -in myapp.local.crt -noout -text | grep -A5 ‘Subject Alternative’ # Should show: DNS:myapp.local, DNS:*.myapp.local, IP Address:127.0.0.1 |
Installing the CA Root in Trust Stores
All clients that need to trust certificates signed by your local CA must have the CA root certificate (ca.crt) installed in their trust store. The CA private key is never distributed.
macOS system trust store (Chrome and system apps)
| # Add CA root to macOS Keychain (system-wide, requires admin password):
$ sudo security add-trusted-cert -d -r trustRoot \ -k /Library/Keychains/System.keychain ~/localCA/certs/ca.crt
# Verify it was added: $ security find-certificate -a -c ‘Local Development CA’ /Library/Keychains/System.keychain
# Chrome on macOS reads the system Keychain automatically. # Restart Chrome after adding the certificate. |
Windows system trust store (Chrome and system apps)
| # Option 1: Via certmgr.msc (GUI):
# Run certmgr.msc, navigate to Trusted Root Certification Authorities, # right-click Certificates, All Tasks, Import, select ca.crt.
# Option 2: Command line (requires admin PowerShell): > Import-Certificate -FilePath ‘C:\path\to\ca.crt’ ` -CertStoreLocation ‘Cert:\LocalMachine\Root’
# Chrome on Windows reads the Windows Certificate Store automatically. # Restart Chrome after importing. |
Linux system trust store
| # Debian/Ubuntu:
$ sudo cp ~/localCA/certs/ca.crt /usr/local/share/ca-certificates/localdev-ca.crt $ sudo update-ca-certificates
# Red Hat/CentOS/Fedora: $ sudo cp ~/localCA/certs/ca.crt /etc/pki/ca-trust/source/anchors/localdev-ca.crt $ sudo update-ca-trust extract
# Chrome on Linux reads the system NSS database. # Also import into the user NSS database for Chrome if needed: $ certutil -d sql:$HOME/.pki/nssdb -A -t ‘C,,’ \ -n ‘Local Development CA’ -i ~/localCA/certs/ca.crt |
Firefox (all platforms)
| # Firefox maintains its own certificate store, separate from the OS.
# Option 1 (GUI): Firefox Settings, Privacy and Security, #Â Â Certificates, View Certificates, Authorities tab, #Â Â Import, select ca.crt, check ‘Trust for websites’.
# Option 2 (command line, using certutil from NSS tools): # Find your Firefox profile directory: $ ls ~/.mozilla/firefox/*.default-release/
$ certutil -d ~/.mozilla/firefox/PROFILENAME/ \ -A -t ‘C,,’ -n ‘Local Development CA’ \ -i ~/localCA/certs/ca.crt |
Trusting the CA Root in Docker Containers and Kubernetes
Development environments using Docker or Kubernetes need the CA root available inside containers. Applications running in containers cannot access the host machine’s trust store.
Dockerfile approach
| # Inject the CA root certificate into a Docker image:
# (Debian/Ubuntu-based image): COPY ./localCA/certs/ca.crt /usr/local/share/ca-certificates/localdev-ca.crt RUN update-ca-certificates
# Alpine Linux: COPY ./localCA/certs/ca.crt /usr/local/share/ca-certificates/localdev-ca.crt RUN update-ca-certificates
# Red Hat/CentOS-based: COPY ./localCA/certs/ca.crt /etc/pki/ca-trust/source/anchors/localdev-ca.crt RUN update-ca-trust extract
# Only copy ca.crt (public certificate), NEVER copy ca.key into an image. |
Docker Compose volume mount
| # Alternative: mount the CA cert as a volume at runtime
# (avoids baking the cert into the image, useful for rotating CAs) services: myapp: image: myapp:latest volumes: – ./localCA/certs/ca.crt:/usr/local/share/ca-certificates/localdev-ca.crt:ro # The app still needs to run update-ca-certificates on startup # or use language-specific CA bundle configuration
# Python: set SSL_CERT_FILE or REQUESTS_CA_BUNDLE env var # Node.js: set NODE_EXTRA_CA_CERTS=/path/to/ca.crt # Go: system trust store is used by default after update-ca-certificates # Java: import into the JDK cacerts keystore with keytool |
Using Your Local CA for Microservice mTLS
When developing microservices that communicate over mTLS (mutual TLS), your local CA can issue both server certificates and client certificates. Each service presents a certificate to authenticate itself to other services.
The process is the same as issuing server certificates, but with different extended key usage. For client certificates (used when a service authenticates to another service as a client), add clientAuth to the extendedKeyUsage in the signing extensions. For server certificates, keep serverAuth. For certificates that serve both roles (services that both initiate and receive connections), include both serverAuth and clientAuth.
The mutual trust is established by having all services trust the same CA root. When service A connects to service B, B presents its server certificate (issued by your local CA) and A verifies it against the CA root. Simultaneously, A presents its client certificate (also issued by your local CA) and B verifies it. Both services trust the CA; therefore both trust each other’s certificates.
Frequently Asked Questions
What is the difference between mkcert and a manual OpenSSL CA?
mkcert is a developer convenience tool that automates local CA creation and certificate issuance, and automatically installs the CA root into your OS trust store, Firefox, and Java. The manual OpenSSL CA approach gives you full control: you manage the CA configuration, extension settings, certificate properties, and distribution. mkcert is appropriate for individual developers who need localhost HTTPS quickly. A manual OpenSSL CA is appropriate for teams, container environments, mTLS setups, and situations where you need non-standard certificate properties or want to understand the CA mechanics.
Why do my manually created certificates still fail in browsers?
The most common cause is missing Subject Alternative Name entries. Modern browsers require the connecting hostname to appear in the certificate’s SAN extension (dNSName or iPAddress entries). If your CA configuration does not include copy_extensions = copy, SAN entries from the CSR are stripped during signing. The result has a correct Common Name but no SAN, which browsers reject. Verify with: openssl x509 -in cert.crt -noout -text | grep -A5 ‘Subject Alternative Name’. If no SAN entries appear, regenerate the certificate with the copy_extensions setting in your CA config and the -addext flag on the CSR generation command.
How do I share my local CA with my development team?
Distribute only the CA root certificate (ca.crt), never the CA private key. Each team member imports ca.crt into their OS and browser trust stores using the platform-specific steps in this guide. With the CA root trusted, any certificate issued by that CA (which you generate using the private key only you hold) will be trusted on all team members’ machines. Store the CA private key in a secure location such as a password manager’s secure notes, an encrypted disk image, or a secrets management system. If the key is lost or compromised, a new CA must be created and the root redistributed.
How long should my local CA root certificate be valid?
For a development CA, 10 years (3650 days) is a common choice. This avoids the operational overhead of rotating the CA root and redistributing it to all team members and containers. For the server certificates signed by the CA, 825 days or less is recommended to stay below the informal maximum that many TLS libraries apply. Short-lived server certs (90 days) with automated local renewal are more secure but add operational complexity. For most development environments, 825-day server certificates are a reasonable balance.
