Some checks failed
Architecture Governance / architecture (push) Failing after 6m16s
Reconcile TLS, exact CORS, and backend proxying before release activation so every storefront uses its derived API host.
137 lines
4.2 KiB
Bash
Executable File
137 lines
4.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Attach one customer domain to this server and issue a TLS certificate.
|
|
# Idempotent: re-running for an existing domain renews/repairs rather than duplicates.
|
|
# Run as root, AFTER the domain's A/AAAA record already resolves to this server.
|
|
#
|
|
# bash add-domain.sh shop.example.com --email ops@example.com
|
|
# bash add-domain.sh shop.example.com --email ops@example.com --with-www
|
|
#
|
|
# Why per-domain blocks exist at all: the application is multi-tenant off the
|
|
# Host header and needs no per-domain root. Certificates are the exception —
|
|
# certbot must match a concrete server_name, which `default_server _` is not.
|
|
|
|
set -euo pipefail
|
|
|
|
DOMAIN="${1:-}"; shift || true
|
|
EMAIL=""
|
|
WITH_WWW=0
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--email) EMAIL="$2"; shift 2 ;;
|
|
--with-www) WITH_WWW=1; shift ;;
|
|
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
[[ $EUID -eq 0 ]] || { echo "must run as root" >&2; exit 1; }
|
|
[[ -n "$DOMAIN" ]] || { echo "usage: add-domain.sh <domain> --email <address> [--with-www]" >&2; exit 2; }
|
|
[[ -n "$EMAIL" ]] || { echo "--email is required (certbot expiry notices)" >&2; exit 2; }
|
|
|
|
# Fail loudly rather than let certbot fail obscurely on an unpointed domain.
|
|
echo "==> checking DNS for $DOMAIN"
|
|
RESOLVED="$(getent hosts "$DOMAIN" | awk '{print $1}' | head -1 || true)"
|
|
if [[ -z "$RESOLVED" ]]; then
|
|
echo "ERROR: $DOMAIN does not resolve. Point its A record at this server first." >&2
|
|
exit 1
|
|
fi
|
|
echo " resolves to $RESOLVED"
|
|
|
|
NAMES="$DOMAIN"
|
|
CERT_ARGS=(-d "$DOMAIN")
|
|
if [[ $WITH_WWW -eq 1 ]]; then
|
|
NAMES="$DOMAIN www.$DOMAIN"
|
|
CERT_ARGS+=(-d "www.$DOMAIN")
|
|
fi
|
|
|
|
CONF="/etc/nginx/sites-available/tenant-$DOMAIN.conf"
|
|
echo "==> nginx server block: $CONF"
|
|
cat > "$CONF" <<NGINX
|
|
# Tenant domain: $DOMAIN
|
|
# Same root as the catch-all — the SPA resolves the tenant from the Host header.
|
|
# This block exists so certbot has a concrete server_name to attach TLS to.
|
|
|
|
server {
|
|
listen 80;
|
|
listen [::]:80;
|
|
server_name $NAMES;
|
|
|
|
root /srv/marketplaces/current/frontend;
|
|
index index.html;
|
|
|
|
location = /index.html {
|
|
add_header Cache-Control "no-store, must-revalidate" always;
|
|
try_files \$uri =404;
|
|
}
|
|
|
|
location ~* \.(js|css|woff2?|png|jpe?g|svg|gif|webp|avif|ico)\$ {
|
|
expires 1y;
|
|
add_header Cache-Control "public, immutable" always;
|
|
try_files \$uri =404;
|
|
}
|
|
|
|
location /health {
|
|
access_log off;
|
|
return 200 "ok\n";
|
|
add_header Content-Type text/plain;
|
|
}
|
|
|
|
location /api/ {
|
|
proxy_pass http://127.0.0.1:8080;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host \$host;
|
|
proxy_set_header X-Real-IP \$remote_addr;
|
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
proxy_read_timeout 60s;
|
|
}
|
|
|
|
location / {
|
|
try_files \$uri \$uri/ /index.html;
|
|
}
|
|
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
|
|
gzip on;
|
|
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
|
gzip_min_length 1024;
|
|
}
|
|
NGINX
|
|
|
|
ln -sfn "$CONF" "/etc/nginx/sites-enabled/tenant-$DOMAIN.conf"
|
|
nginx -t
|
|
systemctl reload nginx
|
|
|
|
echo "==> certificate"
|
|
# --nginx rewrites the block above in place to add listen 443 + ssl directives
|
|
# and an HTTP->HTTPS redirect. Re-running is a no-op when the cert is current.
|
|
certbot --nginx "${CERT_ARGS[@]}" \
|
|
--non-interactive --agree-tos --email "$EMAIL" \
|
|
--redirect --keep-until-expiring
|
|
|
|
nginx -t
|
|
systemctl reload nginx
|
|
|
|
echo "==> companion API domain(s)"
|
|
CONFIGURE_API="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/configure-api-domain.sh"
|
|
[[ -x "$CONFIGURE_API" ]] || {
|
|
echo "ERROR: configure-api-domain.sh must be executable and next to add-domain.sh" >&2
|
|
exit 1
|
|
}
|
|
"$CONFIGURE_API" --domain "$DOMAIN" --email "$EMAIL"
|
|
if [[ $WITH_WWW -eq 1 ]]; then
|
|
"$CONFIGURE_API" --domain "www.$DOMAIN" --email "$EMAIL"
|
|
fi
|
|
|
|
echo "==> renewal timer"
|
|
systemctl enable --now certbot.timer
|
|
systemctl status certbot.timer --no-pager | head -3 || true
|
|
|
|
echo
|
|
echo "done. verify:"
|
|
echo " curl -I https://$DOMAIN/health"
|
|
echo " certbot certificates | grep -A3 $DOMAIN"
|