Skip to content

TLS

Scope: an HTTPS server block with a self-signed certificate made by hand — enough to learn the whole shape — and an honest boundary at the public-certificate step.

What nginx needs

Two files: a certificate and its private key. Everything else is three directives:

server {
    listen 127.0.0.1:8443 ssl;
    server_name localhost;
    ssl_certificate     /home/you/nginx-lab/prefix/certs/lab.crt;
    ssl_certificate_key /home/you/nginx-lab/prefix/certs/lab.key;
    root /home/you/nginx-lab/www/main;
    index index.html;
}

Making the pair with openssl

Self-signed means "signed by me": a certificate whose issuer is itself. The command (from the OpenSSL for Mere Mortals guide's world; this is its most useful trick):

openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout lab.key -out lab.crt -days 365 \
  -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

The subject alternative name (-addext) is the field modern clients actually check: certifying CN alone no longer convinces anyone. Inspect what you made exactly as a client would:

$ openssl x509 -in lab.crt -noout -subject -issuer -ext subjectAltName
subject=CN = localhost
issuer=CN = localhost
X509v3 Subject Alternative Name:
    DNS:localhost, IP Address:127.0.0.1

Subject equals issuer — the signature is its own. Keep the key file private (req creates it mode 600); possession of the key is the identity the certificate asserts.

What a client makes of it

$ curl -s https://localhost:8443/hello.txt
curl exit: 60

Exit 60 is curl refusing: the certificate isn't signed by anyone in its trust store. That refusal is TLS working as designed — a real visitor would see the browser equivalent, a full-page warning. Two ways past it, for the two different jobs:

$ curl -sk https://localhost:8443/hello.txt
main root: plain file hello.txt
$ curl -s --cacert lab.crt https://localhost:8443/hello.txt
main root: plain file hello.txt

-k skips verification (fine for a loopback lab, reckless anywhere else — it accepts anyone's certificate). --cacert trusts this certificate, which is the honest test: verification happens, the name matches the SAN list, the chain is this one file.

Under the hood the handshake is as modern as the library allows:

$ openssl s_client -connect 127.0.0.1:8443 </dev/null
subject=CN = localhost
issuer=CN = localhost
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384

TLS 1.3 required no tuning in the config — the defaults in this nginx build negotiate it.

The HTTP→HTTPS redirect

The last piece of a real HTTPS site is the plain-HTTP server that does nothing but redirect (chapter 8's capture, in its working clothes):

$ curl -sI http://127.0.0.1:8082/hello.txt
HTTP/1.1 301 Moved Permanently
Location: https://127.0.0.1:8443/hello.txt

The step the lab can't take (documented)

A certificate strangers' browsers trust must be issued by a public certificate authority — almost always Let's Encrypt — and issuance requires proving control of a public domain name over the public internet (the ACME protocol: an HTTP challenge served from your domain, or a DNS record). A loopback lab has neither a public name nor reachability, so this guide stops at the honest line: the server block, certificate directives, and client behaviour are all captured above, and the issued certificate drops into exactly the same two directives. In practice on Ubuntu you'd use certbot, which obtains the certificate and edits the server block for you; the loop then is: certbot issues → files land in /etc/letsencrypt/live/<domain>/ → nginx -t → reload → renewals handled by certbot's timer. The Caddy guide shows the same destination by its automatic road.

Check: --cacert against your own lab cert is the full verification path in miniature: name check, chain check, expiry check. If any one fails, curl says which — that vocabulary is the same one public-site debugging uses.