Issuing a Certificate From a Private CA and Applying It to nginx
Goal
You create a private CA, issue a server certificate that includes a SAN, build a chain file, apply HTTPS to nginx, and become able to build an expiry monitoring script as well.
Why it matters
Most certificate outages come not from cryptography but from two things: a missing chain and expiry. If you leave out the intermediate certificate, it works in the browser but fails only in the integrated counterpart system. That is because browsers cache intermediate certificates or fetch them on their own, but server-to-server HTTP clients don't. "It works on my developer PC" is the typical symptom. And expiry doesn't come without warning, yet every year there are incidents because of owner changes and missed alerts. If you build it yourself once with a private CA, this structure stays in your hands.
Steps
- Create the
/root/ng/tlsdirectory and generate a 2048-bit RSA private key as/root/ng/tls/server.key. - Create a CSR with that key and save it as
/root/ng/tls/server.csr. The subject must includeCN=labhub.local,O=LabHubandC=KR. - Create a private CA.
/root/ng/tls/ca.keyand/root/ng/tls/ca.crt. The subject of the CA certificate isCN=LabHub Root CA, the validity must be 3650 days or more, andbasicConstraintsmust haveCA:TRUE. - Sign the CSR with the CA to create
/root/ng/tls/server.crt. The SAN must contain bothDNS:labhub.localandIP:127.0.0.1, and the issuer must be the same as the CA's subject. - Create
/root/ng/tls/fullchain.pem. Concatenate in the order server certificate → CA certificate, and the file must contain exactly 2 certificates.openssl verify -CAfile ca.crt server.crtmust succeed. - Add an HTTPS server block on port 8443 to nginx.
Set
ssl_certificateto fullchain andssl_certificate_keyto server.key, and forssl_protocolsallow onlyTLSv1.2 TLSv1.3. The output ofopenssl s_client -connect 127.0.0.1:8443 -CAfile /root/ng/tls/ca.crtmust showVerify return code: 0. - Make all requests on port 8088 be redirected with a 301 to
https://. The result ofcurl -s -o /dev/null -w '%{http_code} %{redirect_url}' http://127.0.0.1:8088/xmust be301, and the redirect URL must start withhttps://and contain8443. - Create
/root/ng/certcheck.sh. It takes two arguments (인증서파일 임계일수, the certificate file and the threshold in days) and ends with exit code 0 if the remaining validity is more than the threshold days, and with a non-zero exit code if expiry is within the threshold days. If the certificate file doesn't exist, it must also end with a non-zero exit code.
Notes
- Key generation:
openssl genrsa -out server.key 2048 - CSR:
openssl req -new -key server.key -out server.csr -subj "/C=KR/O=LabHub/CN=labhub.local" - Self-signed CA:
openssl req -x509 -new -key ca.key -days 3650 -out ca.crt -subj "..."(you can give the CA extension with-addext "basicConstraints=critical,CA:TRUE") - Injecting a SAN when signing: create an extension file and use
openssl x509 -req ... -extfile <파일>(the extension file) - Checking expiry:
openssl x509 -in <파일> -noout -checkend <초>(the certificate file, then seconds) - Common mistake 1: putting in only a CN without a SAN and repeating "why does the browser reject this".
- Common mistake 2: making the fullchain order CA first. The server certificate comes first.
- Common mistake 3: passing the argument of
-checkendin 'days'. It is in seconds.
Generate the server private key
Create the /root/ng/tls directory and generate a 2048-bit RSA private key as
/root/ng/tls/server.key.
2048-bit RSA is still the standard choice. Also learn the openssl subcommand that checks that the generated key is valid.
Generate the CSR
Create a CSR with that key and save it as /root/ng/tls/server.csr.
The subject must include CN=labhub.local, O=LabHub and C=KR.
You can give the CSR subject information all at once with -subj. The country code is two uppercase letters.
Create a private CA
Create a private CA. /root/ng/tls/ca.key and /root/ng/tls/ca.crt.
The subject of the CA certificate is CN=LabHub Root CA, the validity must be 3650 days or more, and
basicConstraints must have CA:TRUE.
A CA certificate must differ from an ordinary server certificate. Check which extension marks it as a CA. If you give -x509 to openssl req, you get a self-signed certificate.
Sign the server certificate with the CA
Sign the CSR with the CA to create /root/ng/tls/server.crt.
The SAN must contain both DNS:labhub.local and IP:127.0.0.1, and
the issuer must be the same as the CA's subject.
Modern browsers and libraries look at the SAN, not the CN. When signing with openssl x509, you must pass the extension as a file for the SAN to be included.
Build the chain file
Create /root/ng/tls/fullchain.pem.
Concatenate in the order server certificate → CA certificate, and the file must contain exactly 2 certificates.
openssl verify -CAfile ca.crt server.crt must succeed.
Concatenate the server certificate and the CA certificate. The order matters. After creating it, confirm with the verification command that the trust path actually holds.
Apply HTTPS to nginx
Add an HTTPS server block on port 8443 to nginx.
Set ssl_certificate to fullchain and ssl_certificate_key to server.key, and
for ssl_protocols allow only TLSv1.2 TLSv1.3.
The output of openssl s_client -connect 127.0.0.1:8443 -CAfile /root/ng/tls/ca.crt
must show Verify return code: 0.
If the certificate and key are not a pair, nginx doesn't even start. Specify the protocols leaving out the deprecated versions.
HTTP → HTTPS redirect
Make all requests on port 8088 be redirected with a 301 to https://.
The result of curl -s -o /dev/null -w '%{http_code} %{redirect_url}' http://127.0.0.1:8088/x
must be 301, and the redirect URL must start with https:// and contain 8443.
return is clearer and faster than rewrite. Both the status code and the Location header must be right.
Expiry monitoring script
Create /root/ng/certcheck.sh. It takes two arguments (인증서파일 임계일수, the certificate file and the threshold in days) and
ends with exit code 0 if the remaining validity is more than the threshold days,
and with a non-zero exit code if expiry is within the threshold days.
If the certificate file doesn't exist, it must also end with a non-zero exit code.
openssl x509 has an option that tells you through the exit code 'whether the remaining validity is N seconds or more'. If a monitoring script uses it, no date parsing is needed.