-
-
Notifications
You must be signed in to change notification settings - Fork 0
TLS Certificates
IRIS-NG terminates TLS at the iriswebapp_nginx container. By default it uses the
self-signed certificate generated by scripts/generate_dev_certs.sh — good for a lab,
and the reason your browser shows a warning on first login.
You can point it at your own certificate instead — Let's Encrypt, an internal CA, or a commercial cert — without rebuilding any image.
Three settings in .env combine into the path nginx opens:
| Setting | Meaning | Default |
|---|---|---|
CERT_DIR |
Directory on the host holding the cert and key. Bind-mounted read-only at /www/certs/. |
./certificates/web_certificates |
CERT_FILENAME |
Certificate path, relative to CERT_DIR — may include subdirectories. |
iris_dev_cert.pem |
KEY_FILENAME |
Private key path, relative to CERT_DIR. |
iris_dev_key.pem |
SERVER_NAME |
Hostname nginx serves. Must match the certificate. | iris.app.dev |
nginx always reads /www/certs/<CERT_FILENAME>. Only the host side of the mount moves.
CERT_DIRis read by Docker Compose, not by nginx. Do not add it to the nginxenvironment:list or to theenvsubstallowlist indocker/nginx/entrypoint.sh— it is not a template placeholder.
Nothing to configure. If the files are missing:
bash scripts/generate_dev_certs.sh
docker compose -f docker-compose.dev.yml up -d --force-recreate --no-deps nginxSet the three values in .env and recreate nginx. No rebuild is needed.
CERT_DIR=/etc/ssl/iris-ng
CERT_FILENAME=iris.example.com.crt
KEY_FILENAME=iris.example.com.key
SERVER_NAME=iris.example.comdocker compose up -d --force-recreate --no-deps nginxCERT_FILENAME should point at the full chain (server certificate followed by any
intermediates). A server certificate alone will validate in a browser that already holds
the intermediate cached, and fail for everyone else and for API clients — an intermittent
failure that is unpleasant to diagnose.
Point CERT_DIR at the whole /etc/letsencrypt tree, not at live/<domain>:
CERT_DIR=/etc/letsencrypt
CERT_FILENAME=live/iris.example.com/fullchain.pem
KEY_FILENAME=live/iris.example.com/privkey.pem
SERVER_NAME=iris.example.comThe files under live/<domain>/ are symlinks into ../../archive/<domain>/. If you
mount only live/<domain>, the symlink targets fall outside the mount and are dangling
inside the container — nginx cannot read the certificate even though ls shows the file
present on the host.
nginx will tell you so on startup rather than failing obscurely:
FATAL: certificate is a symlink whose target does not exist inside the container:
/www/certs/live/iris.example.com/fullchain.pem
nginx runs inside the container as www-data (uid 33). certbot creates
/etc/letsencrypt/archive/ as 0700 root:root, so the private key is unreadable by
default and nginx exits with:
FATAL: private key exists but is not readable by www-data
Grant traversal and read access on the host. Least-privilege option, using an ACL for uid 33 only:
sudo setfacl -m u:33:rx /etc/letsencrypt/live /etc/letsencrypt/archive
sudo setfacl -m u:33:r /etc/letsencrypt/archive/iris.example.com/privkey*.pemThe blunter alternative is chmod 0755 on those two directories, which exposes every
private key on the host to every local user. Prefer the ACL.
Re-apply after certbot creates a new archive file, or use the deploy hook below to do both in one place.
certbot repoints the symlink; nginx keeps serving the certificate it already has open. Your site will silently continue presenting the expired certificate until nginx is reloaded. Add a deploy hook:
sudo certbot renew \
--deploy-hook 'setfacl -m u:33:r /etc/letsencrypt/archive/iris.example.com/privkey*.pem && docker compose -f /path/to/iris-ng/docker-compose.yml exec -T nginx nginx -s reload'A container restart works too, and costs a few seconds of downtime:
docker compose restart nginx# What nginx is actually presenting
echo | openssl s_client -connect localhost:443 -servername iris.example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
# Chain completeness — "Verify return code: 0 (ok)" means the chain is complete
echo | openssl s_client -connect localhost:443 -servername iris.example.com 2>&1 | grep "Verify return code"Check the mount resolved to the directory you meant:
docker inspect iriswebapp_nginx --format '{{range .Mounts}}{{if eq .Destination "/www/certs"}}{{.Source}}{{end}}{{end}}'nginx validates the certificate and key before starting and reports the resolved path
alongside the variable that produced it. Read docker logs iriswebapp_nginx first — the
message names the cause.
| Message | Cause |
|---|---|
certificate not found |
CERT_DIR points at the wrong directory, or CERT_FILENAME is wrong. The log prints both. |
symlink whose target does not exist inside the container |
Let's Encrypt live/ mounted without archive/. Point CERT_DIR at /etc/letsencrypt. |
not readable by www-data |
Host permissions. See Permissions. |
CERT_FILENAME is not set |
Variable missing from .env, or .env not being read (check you are running compose from the repo root). |
| Browser warns despite a valid certificate |
SERVER_NAME does not match the certificate's CN/SAN. |
| Certificate is valid but clients intermittently fail |
CERT_FILENAME points at the server certificate rather than the full chain. |
The container has restart: always, so a misconfiguration produces a crash loop and the
message repeats — that is expected, and the log is still the place to look.
The Helm chart does not use these variables. TLS is terminated at the Ingress, via the
standard tls.secretName field — see Kubernetes.