Some checks failed
Architecture Governance / architecture (push) Has been cancelled
157 lines
5.8 KiB
Bash
157 lines
5.8 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# Issue ONE wildcard certificate covering every tenant subdomain under an apex.
|
|
# After this, a new tenant at <slug>.<apex> needs no certificate work at all —
|
|
# DNS record, and it is live over HTTPS immediately.
|
|
#
|
|
# setup-wildcard-tls.sh --apex marketplaces.example.com --email ops@example.com --dns cloudflare
|
|
# setup-wildcard-tls.sh --apex marketplaces.example.com --email ops@example.com --dns manual
|
|
#
|
|
# Wildcards require DNS-01 validation — HTTP-01 cannot issue them. That means
|
|
# certbot must create a _acme-challenge TXT record, which needs either a DNS
|
|
# provider plugin (automatic, renews unattended) or manual intervention every
|
|
# 60-90 days. Prefer a plugin. Use manual only to prove the idea out.
|
|
#
|
|
# Tenants on their OWN domains are not covered by a wildcard; those are handled
|
|
# per-domain by sync-domains.sh.
|
|
|
|
set -euo pipefail
|
|
|
|
APEX=""; EMAIL=""; DNS_PLUGIN="manual"; CREDS=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--apex) APEX="$2"; shift 2 ;;
|
|
--email) EMAIL="$2"; shift 2 ;;
|
|
--dns) DNS_PLUGIN="$2"; shift 2 ;;
|
|
--creds) CREDS="$2"; shift 2 ;;
|
|
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
[[ $EUID -eq 0 ]] || { echo "must run as root" >&2; exit 1; }
|
|
[[ -n "$APEX" ]] || { echo "--apex is required" >&2; exit 2; }
|
|
[[ -n "$EMAIL" ]] || { echo "--email is required" >&2; exit 2; }
|
|
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
CERT_ARGS=(-d "$APEX" -d "*.$APEX")
|
|
|
|
case "$DNS_PLUGIN" in
|
|
cloudflare)
|
|
apt-get install -y -qq python3-certbot-dns-cloudflare
|
|
[[ -n "$CREDS" ]] || { echo "--creds <file> required for cloudflare (contains the API token)" >&2; exit 2; }
|
|
chmod 600 "$CREDS"
|
|
CERT_ARGS+=(--dns-cloudflare --dns-cloudflare-credentials "$CREDS" --dns-cloudflare-propagation-seconds 30)
|
|
;;
|
|
route53)
|
|
apt-get install -y -qq python3-certbot-dns-route53
|
|
CERT_ARGS+=(--dns-route53) # credentials come from the instance role or ~/.aws
|
|
;;
|
|
manual)
|
|
cat >&2 <<'WARN'
|
|
WARNING: manual DNS-01.
|
|
|
|
certbot will print a TXT record for you to create by hand, and will do so again
|
|
at every renewal (every 60-90 days). Unattended renewal will NOT work. This is
|
|
acceptable to prove the setup out; it is not acceptable as the steady state.
|
|
|
|
Hostinger has no certbot plugin. If DNS lives there, the options are: move DNS
|
|
to a provider with a plugin (Cloudflare is free and takes minutes), or drive
|
|
issuance from the Phase 9 domain-automation API instead.
|
|
|
|
WARN
|
|
CERT_ARGS+=(--manual --preferred-challenges dns)
|
|
;;
|
|
*)
|
|
echo "unsupported --dns: $DNS_PLUGIN (cloudflare|route53|manual)" >&2; exit 2 ;;
|
|
esac
|
|
|
|
echo "==> issuing wildcard for $APEX and *.$APEX via $DNS_PLUGIN"
|
|
certbot certonly "${CERT_ARGS[@]}" \
|
|
--agree-tos --email "$EMAIL" --keep-until-expiring \
|
|
$([[ "$DNS_PLUGIN" != "manual" ]] && echo --non-interactive)
|
|
|
|
LIVE="/etc/letsencrypt/live/$APEX"
|
|
[[ -f "$LIVE/fullchain.pem" ]] || { echo "certificate not found at $LIVE" >&2; exit 1; }
|
|
|
|
echo "==> nginx: TLS on the catch-all, so every subdomain is served immediately"
|
|
cat > /etc/nginx/snippets/marketplaces-wildcard-tls.conf <<SNIPPET
|
|
# Managed by setup-wildcard-tls.sh
|
|
ssl_certificate $LIVE/fullchain.pem;
|
|
ssl_certificate_key $LIVE/privkey.pem;
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_prefer_server_ciphers off;
|
|
ssl_session_cache shared:SSL:10m;
|
|
ssl_session_timeout 1d;
|
|
ssl_stapling on;
|
|
ssl_stapling_verify on;
|
|
SNIPPET
|
|
|
|
cat > /etc/nginx/sites-available/marketplaces-tls.conf <<NGINX
|
|
# Wildcard TLS catch-all for *.$APEX
|
|
# Any tenant subdomain is served here with no per-tenant configuration.
|
|
server {
|
|
listen 443 ssl default_server;
|
|
listen [::]:443 ssl default_server;
|
|
http2 on;
|
|
server_name $APEX *.$APEX;
|
|
|
|
include /etc/nginx/snippets/marketplaces-wildcard-tls.conf;
|
|
|
|
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;
|
|
}
|
|
location /backend/ {
|
|
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;
|
|
}
|
|
location / { try_files \$uri \$uri/ /index.html; }
|
|
|
|
add_header Strict-Transport-Security "max-age=31536000" always;
|
|
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 /etc/nginx/sites-available/marketplaces-tls.conf /etc/nginx/sites-enabled/marketplaces-tls.conf
|
|
nginx -t
|
|
systemctl reload nginx
|
|
systemctl enable --now certbot.timer
|
|
|
|
# Tell sync-domains.sh which names it can skip.
|
|
mkdir -p /etc/marketplaces
|
|
if [[ -f /etc/marketplaces/domains.env ]]; then
|
|
sed -i '/^WILDCARD_APEX=/d' /etc/marketplaces/domains.env
|
|
fi
|
|
echo "WILDCARD_APEX=$APEX" >> /etc/marketplaces/domains.env
|
|
|
|
echo
|
|
echo "done. every <slug>.$APEX is now served over HTTPS with no further action."
|
|
echo "verify: curl -I https://anything.$APEX/health"
|