Step-by-Step Guide to Building a Self-Signed Production-Grade Private CA for HTTPS Testing
In enterprise environments, securing internal services with HTTPS often requires operating a private Public Key Infrastructure (PKI). This approach allows organizations to issue, manage, and revoke certificates independently of public Certificate Authorities.
This article walks through building a production-grade private Certificate Authority (CA) branded as GoMommy Root CA 😁, issuing a server certificate, validating TLS, and preparing artifacts for platform integration. All commands used in the implementation are included end-to-end.
1. Creating the Working Directory Structure
A structured directory layout is essential for CA operations, auditing, and revocation management.
mkdir -p ca_demo/{certs,crl,newcerts,private}
cd ca_demoDirectory purposes:
certs/— issued certificates archivecrl/— certificate revocation listsnewcerts/— OpenSSL signing outputprivate/— CA private keys (sensitive)
Secure the private directory:
chmod 700 privateGenerate a random seed file for cryptographic operations:
openssl rand -out .rnd 10242. Creating the CA Configuration File
Create the OpenSSL CA configuration:
vi ca.cnfInsert the following production-grade configuration:
HOME = .
RANDFILE = $ENV::HOME/.rnd
####################################################################
[ ca ]
default_ca = CA_default
####################################################################
[ CA_default ]
base_dir = .
certificate = $base_dir/ca.pem
private_key = $base_dir/private/caKey.pem
new_certs_dir = $base_dir/newcerts
database = $base_dir/index.txt
serial = $base_dir/serial.txt
crlnumber = $base_dir/crlnumber.txt
crl = $base_dir/crl/ca.crl.pem
default_days = 3650
default_crl_days = 30
default_md = sha256
preserve = no
copy_extensions = copy
policy = signing_policy
x509_extensions = v3_ca
####################################################################
[ req ]
default_bits = 4096
prompt = no
distinguished_name = dn
x509_extensions = v3_ca
####################################################################
[ dn ]
C = US
ST = Arizona
L = Scottsdale
O = GoMommy Inc
OU = Certificate Authority
CN = GoMommy Root CA
emailAddress = ca@gomommy.com
####################################################################
[ signing_policy ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
####################################################################
[ signing_req ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
basicConstraints = critical,CA:FALSE
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth,clientAuth
####################################################################
[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical,CA:true,pathlen:1
keyUsage = critical,keyCertSign,cRLSignThis configuration enforces SAN copying, CRL support, and strict key usage — all required for production PKI.
3. Initializing the CA Database
Initialize CA tracking files:
touch index.txt
echo '01' > serial.txt
echo '01' > crlnumber.txt4. Generating the Root CA Private Key
Create the CA keypair:
openssl genrsa -out private/caKey.pem 4096
chmod 400 private/caKey.pemA 4096-bit key is recommended for root authorities.
5. Generate Self-Signed Root CA Certificate
Generate the root certificate:
openssl req -x509 -new -nodes \
-key private/caKey.pem \
-sha256 -days 3650 \
-out ca.pem \
-config ca.cnfGenerate an initial CRL:
openssl ca -gencrl -config ca.cnf -out crl/ca.crl.pemThis becomes the trust anchor for all issued certificates.
6. Generate Wildcard Server Private Key
Server: *.bankabc.co.id
openssl genrsa -out wildcard.bankabc.co.id.key 2048
openssl rsa -in wildcard.bankabc.co.id.key -check7. Creating the CSR Configuration (with SAN)
Create CSR config:
vi wildcard.bankabc.co.id.cnfInsert:
[ req ]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext
[ dn ]
C = ID
ST = Jakarta Selatan
L = Jakarta
O = Bank ABC
OU = IT
CN = *.bankabc.co.id
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = *.bankabc.co.id
DNS.2 = bankabc.co.id
IP.1 = 127.0.0.1adds an IP Subject Alternative Name (SAN) to the certificate.
So the cert becomes valid for:
- *.bankabc.co.id (DNS 1)
- bankabc.co.id (DNS 2)
- 127.0.0.1 (IP)
TLS clients will accept the cert only if the hostname/IP they connect to exists in SAN.
8. Generating the CSR
openssl req -new \
-key wildcard.bankabc.co.id.key \
-out wildcard.bankabc.co.id.csr \
-config wildcard.bankabc.co.id.cnf
openssl req -in wildcard.bankabc.co.id.csr -text -noout9. Signing the Certificate
openssl ca \
-config ca.cnf \
-in wildcard.bankabc.co.id.csr \
-out wildcard.bankabc.co.id.crt \
-extensions signing_req \
-days 3970 \
-batch➜ ca_demo openssl ca -config ca.cnf -in wildcard.bankabc.co.id.csr -out wildcard.bankabc.co.id.crt -extensions signing_req -days 3970 -batch
Using configuration from ca.cnf
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
countryName :PRINTABLE:'ID'
stateOrProvinceName :ASN.1 12:'Jakarta Selatan'
localityName :ASN.1 12:'Jakarta'
organizationName :ASN.1 12:'Bank ABC'
organizationalUnitName:ASN.1 12:'IT'
commonName :ASN.1 12:'*.bankabc.co.id'
Certificate is to be certified until Dec 25 23:22:09 2036 GMT (3970 days)
Write out database with 1 new entries
Database updated10. Confirming Issued Files
ls -l wildcard.bankabc.co.id.*
ls newcerts/➜ ca_demo ls -l wildcard.bankabc.co.id.*
-rwxrwxrwx 1 root root 352 Feb 12 06:20 wildcard.bankabc.co.id.cnf
-rwxrwxrwx 1 root root 6235 Feb 12 06:22 wildcard.bankabc.co.id.crt
-rwxrwxrwx 1 root root 1110 Feb 12 06:21 wildcard.bankabc.co.id.csr
-rwxrwxrwx 1 root root 1704 Feb 12 06:20 wildcard.bankabc.co.id.key
➜ ca_demo ls newcerts/
01.pem11. Inspecting the Certificate
openssl x509 -in wildcard.bankabc.co.id.crt -text -noout
openssl x509 -in wildcard.bankabc.co.id.crt -noout -issuer -subject -dates12. Verifying Trust
openssl verify -CAfile ca.pem wildcard.bankabc.co.id.crtExpected output:
app1.crt: OK13. Building the Certificate Chain
cat wildcard.bankabc.co.id.crt ca.pem > wildcard.bankabc.co.id-chain.crt➜ ca_demo ls --all | grep wildcar
wildcard.bankabc.co.id-chain.crt
wildcard.bankabc.co.id.cnf
wildcard.bankabc.co.id.crt
wildcard.bankabc.co.id.csr
wildcard.bankabc.co.id.keyArtifacts:
- Leaf certificate →
wildcard.bankabc.co.id.crt - Full chain →
wildcard.bankabc.co.id-chain.crt
14. Testing TLS Locally
Start TLS server:
echo "127.0.0.1 api.bankabc.co.id" | sudo tee -a /etc/hostsopenssl s_server \
-cert wildcard.bankabc.co.id.crt \
-key wildcard.bankabc.co.id.key \
-accept 8443From another terminal on the same directory:
openssl s_client \
-connect api.bankabc.co.id:8443 \
-servername api.bankabc.co.id \
-CAfile ca.pemSuccessful handshake confirms certificate validity.
Verify return code: 0 (ok)15. Testing HTTPS via Curl
Launch HTTPS test server:
openssl s_server \
-cert wildcard.bankabc.co.id-chain.crt \
-key wildcard.bankabc.co.id.key \
-accept 8443 -wwwInsecure diagnostic mode:
curl -vk https://api.bankabc.co.id:8443 --cacert ca.pemStrict validation:
curl -v https://api.bankabc.co.id:8443 --cacert ca.pem16. Testing HTTPS via SSL Scan
sslscan api.bankabc.co.id:8443➜ ca_demo sslscan api.bankabc.co.id:8443
Version: 2.1.2
OpenSSL 3.0.13 30 Jan 2024
Connected to 127.0.0.1
Testing SSL server api.bankabc.co.id on port 8443 using SNI name api.bankabc.co.id
SSL/TLS Protocols:
SSLv2 disabled
SSLv3 disabled
TLSv1.0 disabled
TLSv1.1 disabled
TLSv1.2 enabled
TLSv1.3 enabled
TLS Fallback SCSV:
Server supports TLS Fallback SCSV
TLS renegotiation:
Session renegotiation not supported
TLS Compression:
OpenSSL version does not support compression
Rebuild with zlib1g-dev package for zlib support
Heartbleed:
TLSv1.3 not vulnerable to heartbleed
TLSv1.2 not vulnerable to heartbleed
Supported Server Cipher(s):
Preferred TLSv1.3 128 bits TLS_AES_128_GCM_SHA256 Curve 25519 DHE 253
Accepted TLSv1.3 256 bits TLS_AES_256_GCM_SHA384 Curve 25519 DHE 253
Accepted TLSv1.3 256 bits TLS_CHACHA20_POLY1305_SHA256 Curve 25519 DHE 253
Preferred TLSv1.2 256 bits ECDHE-RSA-AES256-GCM-SHA384 Curve 25519 DHE 253
Accepted TLSv1.2 256 bits DHE-RSA-AES256-GCM-SHA384 DHE 2048 bits
Accepted TLSv1.2 256 bits ECDHE-RSA-CHACHA20-POLY1305 Curve 25519 DHE 253
Accepted TLSv1.2 256 bits DHE-RSA-CHACHA20-POLY1305 DHE 2048 bits
Accepted TLSv1.2 128 bits ECDHE-RSA-AES128-GCM-SHA256 Curve 25519 DHE 253
Accepted TLSv1.2 128 bits DHE-RSA-AES128-GCM-SHA256 DHE 2048 bits
Accepted TLSv1.2 256 bits ECDHE-RSA-AES256-SHA384 Curve 25519 DHE 253
Accepted TLSv1.2 256 bits DHE-RSA-AES256-SHA256 DHE 2048 bits
Accepted TLSv1.2 128 bits ECDHE-RSA-AES128-SHA256 Curve 25519 DHE 253
Accepted TLSv1.2 128 bits DHE-RSA-AES128-SHA256 DHE 2048 bits
Accepted TLSv1.2 256 bits ECDHE-RSA-AES256-SHA Curve 25519 DHE 253
Accepted TLSv1.2 256 bits DHE-RSA-AES256-SHA DHE 2048 bits
Accepted TLSv1.2 128 bits ECDHE-RSA-AES128-SHA Curve 25519 DHE 253
Accepted TLSv1.2 128 bits DHE-RSA-AES128-SHA DHE 2048 bits
Accepted TLSv1.2 256 bits AES256-GCM-SHA384
Accepted TLSv1.2 128 bits AES128-GCM-SHA256
Accepted TLSv1.2 256 bits AES256-SHA256
Accepted TLSv1.2 128 bits AES128-SHA256
Accepted TLSv1.2 256 bits AES256-SHA
Accepted TLSv1.2 128 bits AES128-SHA
Server Key Exchange Group(s):
TLSv1.3 128 bits secp256r1 (NIST P-256)
TLSv1.3 192 bits secp384r1 (NIST P-384)
TLSv1.3 260 bits secp521r1 (NIST P-521)
TLSv1.3 128 bits x25519
TLSv1.3 224 bits x448
TLSv1.3 112 bits ffdhe2048
TLSv1.3 128 bits ffdhe3072
TLSv1.3 150 bits ffdhe4096
TLSv1.3 175 bits ffdhe6144
TLSv1.3 192 bits ffdhe8192
TLSv1.2 128 bits secp256r1 (NIST P-256)
TLSv1.2 192 bits secp384r1 (NIST P-384)
TLSv1.2 260 bits secp521r1 (NIST P-521)
TLSv1.2 128 bits x25519
TLSv1.2 224 bits x448
SSL Certificate:
Signature Algorithm: sha256WithRSAEncryption
RSA Key Strength: 2048
Subject: *.bankabc.co.id
Altnames: DNS:*.bankabc.co.id, DNS:bankabc.co.id, IP Address:127.0.0.1
Issuer: GoMommy Root CA
Not valid before: Feb 11 23:22:09 2026 GMT
Not valid after: Dec 25 23:22:09 2036 GMT17. File Permission Hardening
chmod 700 private
chmod 400 private/caKey.pem
chmod 600 wildcard.bankabc.co.id.key
chmod 644 *.crt➜ ca_demo ls -all
total 40
drwxrwxrwx 1 root root 4096 Feb 12 06:04 .
drwxrwxrwx 1 root root 4096 Feb 12 06:01 ..
-rwxrwxrwx 1 root root 1024 Feb 12 06:02 .rnd
-rwxrwxrwx 1 root root 2003 Feb 12 06:02 ca.cnf
-rwxrwxrwx 1 root root 2232 Feb 12 06:02 ca.pem
drwxrwxrwx 1 root root 4096 Feb 12 06:01 certs
drwxrwxrwx 1 root root 4096 Feb 12 06:02 crl
-rwxrwxrwx 1 root root 3 Feb 12 06:02 crlnumber.txt
-rwxrwxrwx 1 root root 3 Feb 12 06:02 crlnumber.txt.old
-rwxrwxrwx 1 root root 99 Feb 12 06:04 index.txt
-rwxrwxrwx 1 root root 21 Feb 12 06:04 index.txt.attr
-rwxrwxrwx 1 root root 0 Feb 12 06:02 index.txt.old
drwxrwxrwx 1 root root 4096 Feb 12 06:04 newcerts
drwxrwxrwx 1 root root 4096 Feb 12 06:02 private
-rwxrwxrwx 1 root root 3 Feb 12 06:04 serial.txt
-rwxrwxrwx 1 root root 3 Feb 12 06:02 serial.txt.old
-rwxrwxrwx 1 root root 8475 Feb 12 06:04 wildcard.bankabc.co.id-chain.crt
-rwxrwxrwx 1 root root 353 Feb 12 06:03 wildcard.bankabc.co.id.cnf
-rwxrwxrwx 1 root root 6243 Feb 12 06:04 wildcard.bankabc.co.id.crt
-rwxrwxrwx 1 root root 1110 Feb 12 06:03 wildcard.bankabc.co.id.csr
-rwxrwxrwx 1 root root 1704 Feb 12 06:02 wildcard.bankabc.co.id.key
18. Files Required for HTTPS Deployment
Keep these for runtime:
- wildcard.bankabc.co.id.crt — server certificate
- wildcard.bankabc.co.id-chain.crt — full chain
- wildcard.bankabc.co.id.key — private key
- ca.pem — trust anchor
All other files belong to CA operations.
