Stunnel PKI — Certificate Hierarchy

A private single-CA mutual-TLS PKI used by all ansible-managed servers and pomelo. The CA cert doubles as the server identity — there are no per-host certificates. See ssl for overview and ssl/create-certs for the commands to regenerate.

Trust Model

The CA is self-signed. Every managed server receives the CA cert/key and uses them directly as its TLS identity. The server's CAfile points to itself, so verify = 2 means: the client must present a certificate signed by this CA. There is one client certificate (pomelo), signed by the CA. No per-host server certs exist — all servers share the same CA identity.

This keeps the PKI simple: rotating the CA cert means updating one file on all servers and on pomelo. The tradeoff is that any server can impersonate any other server — acceptable for a single-operator setup.

Certificate Inventory

RoleSource on pomeloDeployed to (remote)Deployed as
CA cert (= server cert)~/ssl/C/CA/certall stunnel servers/etc/stunnel/server.pem
CA private key (= server key)~/ssl/C/CA/keyall stunnel servers/etc/stunnel/server.key
Client cert~/ssl/C/certpomelo only (manual)/etc/stunnel/cert.pem
Client private key~/ssl/C/keypomelo only (manual)/etc/stunnel/key.pem
CA cert (client trust store)~/ssl/C/CA/certpomelo only (manual)/etc/stunnel/CA.pem

Certificate Details

CA cert: CN=John Critchley, self-signed, valid 2025-09-01 to 2035-09-02.

Client cert: CN=John Critchley, signed by CA, valid 2025-09-02 to 2035-09-03.

Both certs use the same CN — checkHost = John Critchley in the client config verifies the server's CN. This works because the server cert IS the CA cert.

Server-side stunnel.conf (deployed by ansible)

cert   = /etc/stunnel/server.pem
key    = /etc/stunnel/server.key
CAfile = /etc/stunnel/server.pem   ; same file — server verifies clients against itself
verify = 2

[socks5h]
accept  = 11080
connect = 127.0.0.1:1080

[notes]
accept  = 18021
connect = 127.0.0.1:8020

[notes-mcp]
accept  = 18023
connect = 127.0.0.1:8023

Client-side stunnel.conf (pomelo, manual)

verify = 2
CAfile = /etc/stunnel/CA.pem    ; ~/ssl/C/CA/cert
cert   = /etc/stunnel/cert.pem  ; ~/ssl/C/cert
key    = /etc/stunnel/key.pem   ; ~/ssl/C/key

[socks5h]
client = yes
accept = 1080
connect = gravlax:11080
checkHost = John Critchley

[notes]
client = yes
accept = 8021
connect = gravlax:18021
checkHost = John Critchley

[notes-mcp]
client = yes
accept = 8023
connect = gravlax:18023
checkHost = John Critchley

Setting Up a New Server

The server side is fully automated: run ansible-playbook setup_server.yml --tags stunnel -e target=<host>. The playbook copies ~/ssl/C/CA/cert and ~/ssl/C/CA/key from pomelo to the remote host and writes the stunnel.conf. No new certificates are needed.

Setting Up a New Controller (client side)

The client side is manual — not yet in any playbook. On the new controller:

1. Copy ~/ssl/C/CA/cert to /etc/stunnel/CA.pem

2. Copy ~/ssl/C/cert to /etc/stunnel/cert.pem

3. Copy ~/ssl/C/key to /etc/stunnel/key.pem (mode 600)

4. Write /etc/stunnel/stunnel.conf with the client sections (see above), adjusting connect = to the current application server hostname.

5. Enable and start stunnel4.

Security Notes

The CA private key (~/ssl/C/CA/key) must never leave pomelo except to be deployed to managed servers via ansible. It must never appear in notes or git.

The client private key (~/ssl/C/key) must never leave pomelo. Do not deploy it to remote hosts.

version 1  ·  created 2026-06-05  ·  updated 2026-06-05  ·  tags ['ssl', 'tls', 'stunnel', 'pki', 'certificates']