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.