## Intro

A local TLS lab lets you practice certificate generation, server configuration, and debugging without risking production. In this guide you will:

- Create a local certificate authority (CA)

- Issue a server certificate for app.local (127.0.0.1)

- Configure an Nginx reverse proxy with TLS and secure defaults

- Validate with curl and openssl

- Optionally add mutual TLS (client certificates)

- See how to extend the same artifacts to a Kubernetes Ingress

The focus is a narrow, testable setup you can fully inspect on your machine. Keep it local, keep it simple, and add complexity only after you have reliable checks.

## Workflow Overview

A clear, staged workflow reduces rework and speeds up learning:

Choose hostnames that never resolve publicly, for example app.local and api.local . Map them to 127.0.0.1 in your hosts file:

- Name your local endpoints

- Linux/macOS: /etc/hosts

- Windows: C:\Windows\System32\drivers\etc\hosts

Example entries:

127.0.0.1 app.local
 127.0.0.1 api.local 
 You have two common options. Pick one.

- Create a local CA and issue a server certificate

Option A: OpenSSL (works everywhere)

mkdir -p certs
 openssl genrsa -out certs/rootCA.key 4096
 openssl req -x509 -new -nodes -key certs/rootCA.key -sha256 -days 3650 -out certs/rootCA.crt -subj "/CN=lab-root-ca"
 openssl genrsa -out certs/app.local.key 2048
 openssl req -new -key certs/app.local.key -out certs/app.local.csr -subj "/CN=app.local"
 cat > certs/app.local.ext <<'EOF'
 subjectAltName = @alt_names
 basicConstraints = CA:FALSE
 keyUsage = digitalSignature, keyEncipherment
 extendedKeyUsage = serverAuth
 [alt_names]
 DNS.1 = app.local
 DNS.2 = localhost
 IP.1 = 127.0.0.1
 EOF
 openssl x509 -req -in certs/app.local.csr -CA certs/rootCA.crt -CAkey certs/rootCA.key -CAcreateserial -out certs/app.local.crt -days 825 -sha256 -extfile certs/app.local.ext 
 Option B: mkcert (convenient local CA)

mkcert -install
 mkcert -cert-file certs/app.local.crt -key-file certs/app.local.key app.local 127.0.0.1 localhost 
 mkcert also creates its root CA (location varies by OS) and adds it to the system trust store.

Create a minimal Nginx server that terminates TLS and serves a simple response. Adjust paths as needed.

- Configure Nginx with TLS

nginx.conf (or a site file you include):

server {
 listen 443 ssl http2;
 server_name app.local;

 ssl_certificate /absolute/path/certs/app.local.crt;
 ssl_certificate_key /absolute/path/certs/app.local.key;

 ssl_protocols TLSv1.2 TLSv1.3;
 ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
 ssl_prefer_server_ciphers on;
 ssl_session_cache shared:SSL:10m;
 ssl_session_timeout 1h;

 # HSTS for lab: safe if you only use app.local locally
 add_header Strict-Transport-Security "max-age=31536000" always;

 location / {
 return 200 'hello tls lab\n';
 add_header Content-Type text/plain;
 }
 }

 server {
 listen 80;
 server_name app.local;
 return 301 https://$host$request_uri;
 } 
 Reload Nginx after testing the config:

nginx -t && nginx -s reload
 # or with systemd
 # sudo systemctl reload nginx 
 You can trust your root CA system-wide (optional) or point tools to it explicitly.

- Validate with curl and OpenSSL

System trust (optional; requires admin privileges):

- macOS:

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain certs/rootCA.crt 

- Debian/Ubuntu:

 sudo cp certs/rootCA.crt /usr/local/share/ca-certificates/lab-root-ca.crt
 sudo update-ca-certificates 

- Windows (Run as Administrator in cmd or PowerShell):

 certutil -addstore -f "Root" certs\rootCA.crt 
 Direct validation without system trust:

# Use hosts file and CA explicitly
 curl -vkI https://app.local --cacert certs/rootCA.crt

 # Inspect the served certificate
 openssl s_client -connect app.local:443 -servername app.local -showcerts < /dev/null | openssl x509 -noout -subject -issuer -dates 

## Optional: enable mutual TLS (client certs)

 Create a client certificate signed by the same CA:

openssl genrsa -out certs/client.key 2048
openssl req -new -key certs/client.key -out certs/client.csr -subj "/CN=lab-client"
cat > certs/client.ext <<'EOF'
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
EOF
openssl x509 -req -in certs/client.csr -CA certs/rootCA.crt -CAkey certs/rootCA.key -CAcreateserial -out certs/client.crt -days 365 -sha256 -extfile certs/client.ext 
 Nginx mTLS snippet:

ssl_client_certificate /absolute/path/certs/rootCA.crt;
ssl_verify_client optional; # or 'on' to require all requests

location /secure/ {
 if ($ssl_client_verify != SUCCESS) { return 403; }
 return 200 'hello mtls client\n';
 add_header Content-Type text/plain;
} 
 Test mTLS:

# Without client cert: expect 403 for /secure/
curl -vk https://app.local/secure/ --cacert certs/rootCA.crt

# With client cert: expect 200
curl -vk https://app.local/secure/ --cacert certs/rootCA.crt --cert certs/client.crt --key certs/client.key 

## Extend to Kubernetes Ingress (optional)

 If you already run a local cluster (kind, minikube, etc.) with an Ingress controller, reuse the same cert and key.

Create a TLS secret:

kubectl create namespace demo
kubectl -n demo create secret tls app-local-tls --cert=certs/app.local.crt --key=certs/app.local.key 
 Ingress manifest (nginx Ingress example):

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
 name: app-local
 namespace: demo
 annotations:
 nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
 tls:
 - hosts:
 - app.local
 secretName: app-local-tls
 rules:
 - host: app.local
 http:
 paths:
 - path: /
 pathType: Prefix
 backend:
 service:
 name: app-svc
 port:
 number: 80 
 Point app.local to the Ingress endpoint (NodePort, LoadBalancer, or port-forward). Example with port-forwarding the controller to localhost:

# Adjust names to your controller service
kubectl -n ingress-nginx port-forward svc/ingress-nginx-controller 8443:443
# Then:
curl -vkI https://app.local:8443 --resolve app.local:8443:127.0.0.1 --cacert certs/rootCA.crt 

## Rotate certificates safely

- Issue a new server cert with the same SANs

- Update file paths atomically (or overwrite the files)

- Validate Nginx config and reload

 openssl x509 -in certs/app.local.crt -noout -enddate
nginx -t && nginx -s reload 

## Troubleshooting quick wins

- Hostname mismatch: check SANs with openssl x509 -in certs/app.local.crt -noout -text | grep -A1 "Subject Alternative Name"

- Wrong chain: confirm issuer with -issuer and ensure the server sends the correct cert

- Protocol/cipher: curl -v --tls-max 1.2 https://app.local to force-check versions

- Nginx logs: check error log for handshake or certificate errors

## Local Pilot Plan

 Goal: one hostname ( app.local ) over TLS on localhost with measurable checks.

Scope

- Single server: Nginx on 127.0.0.1

- One hostname: app.local

- One CA, one server cert

Steps

- Add hosts entry for app.local -> 127.0.0.1

- Create a local CA and a server cert (OpenSSL commands above)

- Configure Nginx with TLS

- Validate with curl and OpenSSL

Success criteria (measurable)

- curl -skI https://app.local --cacert certs/rootCA.crt returns HTTP/1.1 or HTTP/2 200 OK for the TLS vhost

- openssl s_client -connect app.local:443 -servername app.local shows a chain with issuer = lab-root-ca and a valid Not After date in the future

- nginx -t succeeds and nginx -s reload completes without errors

Timebox

- 45 to 60 minutes end to end, including validation

Optional stretch

- Add mTLS for /secure/ and verify 403 without a client cert and 200 with one

Next extension after success

- Add api.local as a second hostname using the same CA and repeat the checks

## Conclusion

You built a small, safe TLS lab that you can reuse for testing, troubleshooting, and experiments. You created a local CA, issued server and client certificates, configured Nginx with secure defaults, and validated behavior with curl and OpenSSL. Keep improvements incremental: script cert issuance and rotation, add more hostnames, and optionally reuse the same artifacts in a Kubernetes Ingress. Always define clear, local checks so you can verify each change before moving on.