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 <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
b7bce38131
commit
a52f963f52
@@ -1,9 +1,14 @@
|
|||||||
# Per-tenant dashboard cert paths (default → shared meshkee-dashboards cert).
|
# 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.
|
# 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;
|
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;
|
default /etc/letsencrypt/live/meshkee-dashboards/privkey.pem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,10 +65,15 @@ if map_path.is_file():
|
|||||||
mode = None
|
mode = None
|
||||||
for raw in map_path.read_text().splitlines():
|
for raw in map_path.read_text().splitlines():
|
||||||
line = raw.strip()
|
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"
|
mode = "cert"
|
||||||
continue
|
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"
|
mode = "key"
|
||||||
continue
|
continue
|
||||||
if mode and line and not line.startswith("#") and " " in line:
|
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
|
entries_key[customer] = tenant_key
|
||||||
|
|
||||||
def render_map(name: str, entries: dict[str, str]) -> str:
|
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']};")
|
lines.append(f" default {entries['default']};")
|
||||||
for host in sorted(h for h in entries if h != "default"):
|
for host in sorted(h for h in entries if h != "default"):
|
||||||
lines.append(f" {host} {entries[host]};")
|
lines.append(f" {host} {entries[host]};")
|
||||||
@@ -96,6 +102,7 @@ def render_map(name: str, entries: dict[str, str]) -> str:
|
|||||||
|
|
||||||
content = (
|
content = (
|
||||||
"# Managed by ssl-issue-tenant.sh — per-tenant dashboard TLS paths\n"
|
"# 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)
|
+ render_map("meshkee_dashboard_ssl_cert", entries_cert)
|
||||||
+ "\n\n"
|
+ "\n\n"
|
||||||
+ render_map("meshkee_dashboard_ssl_key", entries_key)
|
+ 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}")
|
print(f"Updated {map_path} for {business} / {customer}")
|
||||||
PY
|
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
|
nginx -t && systemctl reload nginx
|
||||||
echo "$(date -Is) Tenant cert ${CERT_NAME} installed and nginx reloaded"
|
echo "$(date -Is) Tenant cert ${CERT_NAME} installed and nginx reloaded"
|
||||||
|
|||||||
Reference in New Issue
Block a user