#!/usr/bin/env bash # Issue / renew Let's Encrypt for one tenant's dashboard hosts only: # business.{apex} + customer.{apex} # Updates nginx cert map and reloads. Does NOT touch other domains. set -euo pipefail CONF=/etc/meshkee/ssl-sync.env # shellcheck disable=SC1090 source "$CONF" APEX="${SSL_TENANT_APEX:-}" if [[ -z "$APEX" ]]; then echo "$(date -Is) ERROR: SSL_TENANT_APEX not set" exit 1 fi APEX="${APEX,,}" CERT_NAME="${SSL_TENANT_CERT_NAME:-meshkee-dash-${APEX//./-}}" EMAIL="${SSL_EMAIL:-info@meshkee.com}" WEBROOT="${SSL_WEBROOT:-/var/www/certbot}" MAP_FILE="${SSL_DASHBOARD_CERT_MAP:-/etc/nginx/conf.d/meshkee-dashboard-certs-map.conf}" BUSINESS_HOST="business.${APEX}" CUSTOMER_HOST="customer.${APEX}" mkdir -p "$WEBROOT" echo "$(date -Is) Issuing tenant dashboard cert ${CERT_NAME} for ${BUSINESS_HOST} ${CUSTOMER_HOST}" certbot certonly \ --webroot -w "$WEBROOT" \ --cert-name "$CERT_NAME" \ --email "$EMAIL" \ --agree-tos \ --non-interactive \ -d "$BUSINESS_HOST" \ -d "$CUSTOMER_HOST" CERT_DIR="/etc/letsencrypt/live/${CERT_NAME}" if [[ ! -f "${CERT_DIR}/fullchain.pem" ]]; then echo "$(date -Is) ERROR: cert not found at ${CERT_DIR}" exit 1 fi python3 - "$MAP_FILE" "$APEX" "$CERT_NAME" <<'PY' import pathlib import sys map_path = pathlib.Path(sys.argv[1]) apex = sys.argv[2].strip().lower() cert_name = sys.argv[3].strip() cert_dir = f"/etc/letsencrypt/live/{cert_name}" business = f"business.{apex}" customer = f"customer.{apex}" default_cert = "/etc/letsencrypt/live/meshkee-dashboards/fullchain.pem" default_key = "/etc/letsencrypt/live/meshkee-dashboards/privkey.pem" tenant_cert = f"{cert_dir}/fullchain.pem" tenant_key = f"{cert_dir}/privkey.pem" entries_cert: dict[str, str] = {"default": default_cert} entries_key: dict[str, str] = {"default": default_key} if map_path.is_file(): mode = None for raw in map_path.read_text().splitlines(): line = raw.strip() # 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 $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: host, path = line.split(None, 1) host = host.rstrip(";") path = path.rstrip(";") if host != "default": if mode == "cert": entries_cert[host] = path else: entries_key[host] = path entries_cert[business] = tenant_cert entries_cert[customer] = tenant_cert entries_key[business] = tenant_key entries_key[customer] = tenant_key def render_map(name: str, entries: dict[str, str]) -> str: # $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]};") lines.append("}") return "\n".join(lines) 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) + "\n" ) 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"