Sitemap

Step-by-Step Guide to Building a Self-Signed Production-Grade Private CA for HTTPS Testing

7 min readFeb 11, 2026
Press enter or click to view image in full size

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_demo

Directory purposes:

  • certs/ — issued certificates archive
  • crl/ — certificate revocation lists
  • newcerts/ — OpenSSL signing output
  • private/ — CA private keys (sensitive)

Secure the private directory:

chmod 700 private

Generate a random seed file for cryptographic operations:

openssl rand -out .rnd 1024

2. Creating the CA Configuration File

Create the OpenSSL CA configuration:

vi ca.cnf

Insert 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,cRLSign

This 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.txt

4. Generating the Root CA Private Key

Create the CA keypair:

openssl genrsa -out private/caKey.pem 4096
chmod 400 private/caKey.pem

A 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.cnf

Generate an initial CRL:

openssl ca -gencrl -config ca.cnf -out crl/ca.crl.pem

This 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 -check

7. Creating the CSR Configuration (with SAN)

Create CSR config:

vi wildcard.bankabc.co.id.cnf

Insert:

[ 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.1

adds 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 -noout

9. 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 updated

10. 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.pem

11. Inspecting the Certificate

openssl x509 -in wildcard.bankabc.co.id.crt -text -noout
openssl x509 -in wildcard.bankabc.co.id.crt -noout -issuer -subject -dates

12. Verifying Trust

openssl verify -CAfile ca.pem wildcard.bankabc.co.id.crt

Expected output:

app1.crt: OK

13. 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.key

Artifacts:

  • 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/hosts
openssl s_server \
-cert wildcard.bankabc.co.id.crt \
-key wildcard.bankabc.co.id.key \
-accept 8443

From another terminal on the same directory:

openssl s_client \
-connect api.bankabc.co.id:8443 \
-servername api.bankabc.co.id \
-CAfile ca.pem

Successful handshake confirms certificate validity.

Verify return code: 0 (ok)
Press enter or click to view image in full size

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 -www

Insecure diagnostic mode:

curl -vk https://api.bankabc.co.id:8443 --cacert ca.pem

Strict validation:

curl -v  https://api.bankabc.co.id:8443 --cacert ca.pem
Press enter or click to view image in full size

16. 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 GMT

17. 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.

--

--

Danang Priabada
Danang Priabada

Written by Danang Priabada

Sr. Software Engineer · Tech Writer · Connect with me on LinkedIn: https://www.linkedin.com/in/danangpriabada/