From a52f963f5298c9d500de9ea4008bc293b0b8762d Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Wed, 26 Aug 2026 08:49:43 +0330 Subject: [PATCH] Fix per-tenant dashboard SSL map to use SNI and readable certs. Nginx must key the cert map on $ssl_server_name during handshake, and www-data needs ssl-cert group access to Let's Encrypt files when certificates are loaded via variables. Co-authored-by: Cursor --- deploy/meshkee-dashboard-certs-map.conf | 9 +++++++-- deploy/ssl-issue-tenant.sh | 23 ++++++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/deploy/meshkee-dashboard-certs-map.conf b/deploy/meshkee-dashboard-certs-map.conf index e3cad20..9933e17 100644 --- a/deploy/meshkee-dashboard-certs-map.conf +++ b/deploy/meshkee-dashboard-certs-map.conf @@ -1,9 +1,14 @@ # Per-tenant dashboard cert paths (default → shared meshkee-dashboards cert). +# Must key on $ssl_server_name (SNI). $host is empty during the TLS handshake, +# so a $host map always falls through to default. # Tenant rows are appended by deploy/ssl-issue-tenant.sh when Issue SSL runs for one domain. -map $host $meshkee_dashboard_ssl_cert { +# +# Variable ssl_certificate is loaded by nginx workers (www-data), so +# /etc/letsencrypt/{live,archive} must be group-readable by ssl-cert (see ssl-issue-tenant.sh). +map $ssl_server_name $meshkee_dashboard_ssl_cert { default /etc/letsencrypt/live/meshkee-dashboards/fullchain.pem; } -map $host $meshkee_dashboard_ssl_key { +map $ssl_server_name $meshkee_dashboard_ssl_key { default /etc/letsencrypt/live/meshkee-dashboards/privkey.pem; } diff --git a/deploy/ssl-issue-tenant.sh b/deploy/ssl-issue-tenant.sh index 72de4ff..40c1a03 100755 --- a/deploy/ssl-issue-tenant.sh +++ b/deploy/ssl-issue-tenant.sh @@ -65,10 +65,15 @@ if map_path.is_file(): mode = None for raw in map_path.read_text().splitlines(): line = raw.strip() - if line.startswith("map $host $meshkee_dashboard_ssl_cert"): + # Accept legacy $host maps and rewrite them to $ssl_server_name. + if line.startswith("map $ssl_server_name $meshkee_dashboard_ssl_cert") or line.startswith( + "map $host $meshkee_dashboard_ssl_cert" + ): mode = "cert" continue - if line.startswith("map $host $meshkee_dashboard_ssl_key"): + if line.startswith("map $ssl_server_name $meshkee_dashboard_ssl_key") or line.startswith( + "map $host $meshkee_dashboard_ssl_key" + ): mode = "key" continue if mode and line and not line.startswith("#") and " " in line: @@ -87,7 +92,8 @@ entries_key[business] = tenant_key entries_key[customer] = tenant_key def render_map(name: str, entries: dict[str, str]) -> str: - lines = [f"map $host ${name} {{"] + # $ssl_server_name is available during handshake; $host is not. + lines = [f"map $ssl_server_name ${name} {{"] lines.append(f" default {entries['default']};") for host in sorted(h for h in entries if h != "default"): lines.append(f" {host} {entries[host]};") @@ -96,6 +102,7 @@ def render_map(name: str, entries: dict[str, str]) -> str: content = ( "# Managed by ssl-issue-tenant.sh — per-tenant dashboard TLS paths\n" + "# Keyed by $ssl_server_name (SNI); $host is empty during TLS handshake.\n" + render_map("meshkee_dashboard_ssl_cert", entries_cert) + "\n\n" + render_map("meshkee_dashboard_ssl_key", entries_key) @@ -105,5 +112,15 @@ map_path.write_text(content) print(f"Updated {map_path} for {business} / {customer}") PY +# Variable ssl_certificate is loaded by www-data workers — LE dirs must be group-readable. +if getent group ssl-cert >/dev/null 2>&1; then + usermod -aG ssl-cert www-data 2>/dev/null || true + chmod 750 /etc/letsencrypt/live /etc/letsencrypt/archive 2>/dev/null || true + chgrp -R ssl-cert /etc/letsencrypt/live /etc/letsencrypt/archive 2>/dev/null || true + find /etc/letsencrypt/live /etc/letsencrypt/archive -type d -exec chmod 750 {} \; 2>/dev/null || true + find /etc/letsencrypt/archive -type f -name 'privkey*.pem' -exec chmod 640 {} \; 2>/dev/null || true + find /etc/letsencrypt/archive -type f -name 'privkey*.pem' -exec chgrp ssl-cert {} \; 2>/dev/null || true +fi + nginx -t && systemctl reload nginx echo "$(date -Is) Tenant cert ${CERT_NAME} installed and nginx reloaded"