From d3c48a1ed1f015522366c70960512ac1a22ec8c3 Mon Sep 17 00:00:00 2001 From: alisaza Date: Sun, 13 Sep 2026 20:38:19 +0330 Subject: [PATCH] fix(ops): enable HTTPS backoffice and Sentry via Finland relay Ship TLS nginx for backoffice, keep HTTP bootstrap until certs exist, proxy /monitoring to sentry-relay, and tunnel server/edge Sentry through Finland. --- deploy/nginx/backoffice-http-bootstrap.conf | 29 +++++++++++++ deploy/nginx/backoffice.conf | 48 ++++++++++++++++++--- docs/deploy.md | 5 ++- scripts/deploy-finland-to-iran.sh | 8 +++- scripts/deploy-on-vps.sh | 8 +++- sentry.edge.config.ts | 9 ++++ sentry.server.config.ts | 9 ++++ 7 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 deploy/nginx/backoffice-http-bootstrap.conf diff --git a/deploy/nginx/backoffice-http-bootstrap.conf b/deploy/nginx/backoffice-http-bootstrap.conf new file mode 100644 index 0000000..f539199 --- /dev/null +++ b/deploy/nginx/backoffice-http-bootstrap.conf @@ -0,0 +1,29 @@ +# Admin backoffice — HTTP bootstrap until Let's Encrypt exists. +# After: certbot certonly --webroot -w /var/www/html -d backoffice.ghabilee.ir +# deploy switches to deploy/nginx/backoffice.conf (TLS). + +upstream ghabilee_admin { + server 127.0.0.1:3009; + keepalive 16; +} + +server { + listen 80; + listen [::]:80; + server_name backoffice.ghabilee.ir; + + location /.well-known/acme-challenge/ { + root /var/www/html; + } + + location / { + proxy_pass http://ghabilee_admin; + proxy_http_version 1.1; + proxy_set_header Connection ""; + 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; + } +} diff --git a/deploy/nginx/backoffice.conf b/deploy/nginx/backoffice.conf index 9a099b0..bae7e27 100644 --- a/deploy/nginx/backoffice.conf +++ b/deploy/nginx/backoffice.conf @@ -1,17 +1,19 @@ # Admin backoffice — TLS terminated by Nginx, app on 127.0.0.1:3009 -# DNS: point backoffice.ghabilee.ir at this VPS, then: -# certbot --nginx -d backoffice.ghabilee.ir - -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} +# +# Requires: +# - DNS A for backoffice.ghabilee.ir → this VPS +# - certbot cert: /etc/letsencrypt/live/backoffice.ghabilee.ir/ +# - ghabilee site enabled (provides $connection_upgrade map) +# +# Issue / renew: +# certbot certonly --webroot -w /var/www/html -d backoffice.ghabilee.ir upstream ghabilee_admin { server 127.0.0.1:3009; keepalive 16; } +# HTTP → ACME + redirect to HTTPS server { listen 80; listen [::]:80; @@ -21,6 +23,38 @@ server { root /var/www/html; } + location / { + return 301 https://backoffice.ghabilee.ir$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name backoffice.ghabilee.ir; + + ssl_certificate /etc/letsencrypt/live/backoffice.ghabilee.ir/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/backoffice.ghabilee.ir/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + client_max_body_size 10m; + + # Browser Sentry tunnel → Finland (ingest.de is blocked from Iran egress). + location = /monitoring { + proxy_pass https://sentry-relay.ghabilee.ir/monitoring; + proxy_ssl_server_name on; + proxy_http_version 1.1; + proxy_set_header Host sentry-relay.ghabilee.ir; + 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_connect_timeout 5s; + proxy_read_timeout 20s; + proxy_send_timeout 20s; + client_max_body_size 1m; + } + location / { proxy_pass http://ghabilee_admin; proxy_http_version 1.1; diff --git a/docs/deploy.md b/docs/deploy.md index f155305..bad5842 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -2,8 +2,9 @@ 1. Point the DNS record for `backoffice.ghabilee.ir` to the production VPS. 2. Create `/opt/ghabilee-admin/.env` from `.env.example` and set production values. -3. Configure the reverse proxy to forward `backoffice.ghabilee.ir` to `127.0.0.1:3009` and enable TLS. -4. Push to `main` (or run the deploy workflow manually) after the environment is ready. +3. Issue TLS: `certbot certonly --webroot -w /var/www/html -d backoffice.ghabilee.ir` (after ACME path is reachable on port 80). +4. Deploy installs `deploy/nginx/backoffice.conf` when the cert exists (otherwise HTTP bootstrap); app listens on `127.0.0.1:3009`. +5. Push to `main` (or run the deploy workflow manually) after the environment is ready. Deploy Telegram alerts use admin-specific copy in `scripts/notify-deploy.sh` (success → backoffice URL; distinct from the consumer frontend message). diff --git a/scripts/deploy-finland-to-iran.sh b/scripts/deploy-finland-to-iran.sh index f6856f0..f1d4560 100755 --- a/scripts/deploy-finland-to-iran.sh +++ b/scripts/deploy-finland-to-iran.sh @@ -109,8 +109,14 @@ scp -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=acce deploy/docker-compose.production.yml \ "${IRAN_USER}@${IRAN_HOST}:${IRAN_APP}/docker-compose.yml" if [[ -f deploy/nginx/backoffice.conf ]]; then + NGINX_BACKOFFICE=deploy/nginx/backoffice.conf + if ! "${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" 'test -s /etc/letsencrypt/live/backoffice.ghabilee.ir/fullchain.pem'; then + if [[ -f deploy/nginx/backoffice-http-bootstrap.conf ]]; then + NGINX_BACKOFFICE=deploy/nginx/backoffice-http-bootstrap.conf + fi + fi scp -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \ - deploy/nginx/backoffice.conf \ + "$NGINX_BACKOFFICE" \ "${IRAN_USER}@${IRAN_HOST}:/etc/nginx/sites-available/backoffice" "${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" ' ln -sfn /etc/nginx/sites-available/backoffice /etc/nginx/sites-enabled/backoffice diff --git a/scripts/deploy-on-vps.sh b/scripts/deploy-on-vps.sh index 2466bf5..dcd0f52 100755 --- a/scripts/deploy-on-vps.sh +++ b/scripts/deploy-on-vps.sh @@ -21,7 +21,13 @@ fi install -m 0644 "$SRC_DIR/deploy/docker-compose.production.yml" "$APP_DIR/docker-compose.yml" if [ -f "$SRC_DIR/deploy/nginx/backoffice.conf" ]; then - install -m 0644 "$SRC_DIR/deploy/nginx/backoffice.conf" /etc/nginx/sites-available/backoffice + if [ -s /etc/letsencrypt/live/backoffice.ghabilee.ir/fullchain.pem ]; then + install -m 0644 "$SRC_DIR/deploy/nginx/backoffice.conf" /etc/nginx/sites-available/backoffice + elif [ -f "$SRC_DIR/deploy/nginx/backoffice-http-bootstrap.conf" ]; then + install -m 0644 "$SRC_DIR/deploy/nginx/backoffice-http-bootstrap.conf" /etc/nginx/sites-available/backoffice + else + install -m 0644 "$SRC_DIR/deploy/nginx/backoffice.conf" /etc/nginx/sites-available/backoffice + fi ln -sfn /etc/nginx/sites-available/backoffice /etc/nginx/sites-enabled/backoffice nginx -t systemctl reload nginx diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts index c13b69c..58e5252 100644 --- a/sentry.edge.config.ts +++ b/sentry.edge.config.ts @@ -4,6 +4,14 @@ import { shouldDropExpectedProductSentryEvent } from './lib/observability/expect const dsn = process.env.SENTRY_DSN?.trim() || process.env.NEXT_PUBLIC_SENTRY_DSN?.trim() +/** Iran runtime cannot reach ingest.de; send via Finland relay. */ +function sentryTunnel(): string | undefined { + const fromEnv = process.env.SENTRY_TUNNEL?.trim() + if (fromEnv) return fromEnv + if (process.env.NODE_ENV === 'production') return 'https://sentry-relay.ghabilee.ir/monitoring' + return undefined +} + function tracesSampleRate(): number { const raw = process.env.SENTRY_TRACES_SAMPLE_RATE ?? '0.1' const parsed = Number.parseFloat(raw) @@ -17,6 +25,7 @@ export function initEdgeSentry() { Sentry.init({ dsn, + tunnel: sentryTunnel(), environment: process.env.SENTRY_ENVIRONMENT?.trim() || process.env.NODE_ENV || 'production', release: process.env.SENTRY_RELEASE?.trim() || process.env.NEXT_PUBLIC_APP_VERSION || undefined, tracesSampleRate: tracesSampleRate(), diff --git a/sentry.server.config.ts b/sentry.server.config.ts index f9a5e03..d9e5440 100644 --- a/sentry.server.config.ts +++ b/sentry.server.config.ts @@ -4,6 +4,14 @@ import { shouldDropExpectedProductSentryEvent } from './lib/observability/expect const dsn = process.env.SENTRY_DSN?.trim() || process.env.NEXT_PUBLIC_SENTRY_DSN?.trim() +/** Iran runtime cannot reach ingest.de; send via Finland relay. */ +function sentryTunnel(): string | undefined { + const fromEnv = process.env.SENTRY_TUNNEL?.trim() + if (fromEnv) return fromEnv + if (process.env.NODE_ENV === 'production') return 'https://sentry-relay.ghabilee.ir/monitoring' + return undefined +} + function tracesSampleRate(): number { const raw = process.env.SENTRY_TRACES_SAMPLE_RATE ?? '0.1' const parsed = Number.parseFloat(raw) @@ -17,6 +25,7 @@ export function initServerSentry() { Sentry.init({ dsn, + tunnel: sentryTunnel(), environment: process.env.SENTRY_ENVIRONMENT?.trim() || process.env.NODE_ENV || 'production', release: process.env.SENTRY_RELEASE?.trim() || process.env.NEXT_PUBLIC_APP_VERSION || undefined, tracesSampleRate: tracesSampleRate(),