#!/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() if line.startswith("map $host $meshkee_dashboard_ssl_cert"): mode = "cert" continue if 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: lines = [f"map $host ${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" + 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 nginx -t && systemctl reload nginx echo "$(date -Is) Tenant cert ${CERT_NAME} installed and nginx reloaded"