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.
This commit is contained in:
alisaza 2026-09-13 20:38:19 +03:30
parent d2222fe73e
commit d3c48a1ed1
7 changed files with 105 additions and 11 deletions

View File

@ -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;
}
}

View File

@ -1,17 +1,19 @@
# Admin backoffice — TLS terminated by Nginx, app on 127.0.0.1:3009 # 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 # Requires:
# - DNS A for backoffice.ghabilee.ir → this VPS
map $http_upgrade $connection_upgrade { # - certbot cert: /etc/letsencrypt/live/backoffice.ghabilee.ir/
default upgrade; # - ghabilee site enabled (provides $connection_upgrade map)
'' close; #
} # Issue / renew:
# certbot certonly --webroot -w /var/www/html -d backoffice.ghabilee.ir
upstream ghabilee_admin { upstream ghabilee_admin {
server 127.0.0.1:3009; server 127.0.0.1:3009;
keepalive 16; keepalive 16;
} }
# HTTP → ACME + redirect to HTTPS
server { server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
@ -21,6 +23,38 @@ server {
root /var/www/html; 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 / { location / {
proxy_pass http://ghabilee_admin; proxy_pass http://ghabilee_admin;
proxy_http_version 1.1; proxy_http_version 1.1;

View File

@ -2,8 +2,9 @@
1. Point the DNS record for `backoffice.ghabilee.ir` to the production VPS. 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. 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. 3. Issue TLS: `certbot certonly --webroot -w /var/www/html -d backoffice.ghabilee.ir` (after ACME path is reachable on port 80).
4. Push to `main` (or run the deploy workflow manually) after the environment is ready. 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` Deploy Telegram alerts use admin-specific copy in `scripts/notify-deploy.sh`
(success → backoffice URL; distinct from the consumer frontend message). (success → backoffice URL; distinct from the consumer frontend message).

View File

@ -109,8 +109,14 @@ scp -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=acce
deploy/docker-compose.production.yml \ deploy/docker-compose.production.yml \
"${IRAN_USER}@${IRAN_HOST}:${IRAN_APP}/docker-compose.yml" "${IRAN_USER}@${IRAN_HOST}:${IRAN_APP}/docker-compose.yml"
if [[ -f deploy/nginx/backoffice.conf ]]; then 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 \ 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" "${IRAN_USER}@${IRAN_HOST}:/etc/nginx/sites-available/backoffice"
"${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" ' "${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" '
ln -sfn /etc/nginx/sites-available/backoffice /etc/nginx/sites-enabled/backoffice ln -sfn /etc/nginx/sites-available/backoffice /etc/nginx/sites-enabled/backoffice

View File

@ -21,7 +21,13 @@ fi
install -m 0644 "$SRC_DIR/deploy/docker-compose.production.yml" "$APP_DIR/docker-compose.yml" install -m 0644 "$SRC_DIR/deploy/docker-compose.production.yml" "$APP_DIR/docker-compose.yml"
if [ -f "$SRC_DIR/deploy/nginx/backoffice.conf" ]; then if [ -f "$SRC_DIR/deploy/nginx/backoffice.conf" ]; then
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 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 ln -sfn /etc/nginx/sites-available/backoffice /etc/nginx/sites-enabled/backoffice
nginx -t nginx -t
systemctl reload nginx systemctl reload nginx

View File

@ -4,6 +4,14 @@ import { shouldDropExpectedProductSentryEvent } from './lib/observability/expect
const dsn = process.env.SENTRY_DSN?.trim() || process.env.NEXT_PUBLIC_SENTRY_DSN?.trim() 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 { function tracesSampleRate(): number {
const raw = process.env.SENTRY_TRACES_SAMPLE_RATE ?? '0.1' const raw = process.env.SENTRY_TRACES_SAMPLE_RATE ?? '0.1'
const parsed = Number.parseFloat(raw) const parsed = Number.parseFloat(raw)
@ -17,6 +25,7 @@ export function initEdgeSentry() {
Sentry.init({ Sentry.init({
dsn, dsn,
tunnel: sentryTunnel(),
environment: process.env.SENTRY_ENVIRONMENT?.trim() || process.env.NODE_ENV || 'production', environment: process.env.SENTRY_ENVIRONMENT?.trim() || process.env.NODE_ENV || 'production',
release: process.env.SENTRY_RELEASE?.trim() || process.env.NEXT_PUBLIC_APP_VERSION || undefined, release: process.env.SENTRY_RELEASE?.trim() || process.env.NEXT_PUBLIC_APP_VERSION || undefined,
tracesSampleRate: tracesSampleRate(), tracesSampleRate: tracesSampleRate(),

View File

@ -4,6 +4,14 @@ import { shouldDropExpectedProductSentryEvent } from './lib/observability/expect
const dsn = process.env.SENTRY_DSN?.trim() || process.env.NEXT_PUBLIC_SENTRY_DSN?.trim() 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 { function tracesSampleRate(): number {
const raw = process.env.SENTRY_TRACES_SAMPLE_RATE ?? '0.1' const raw = process.env.SENTRY_TRACES_SAMPLE_RATE ?? '0.1'
const parsed = Number.parseFloat(raw) const parsed = Number.parseFloat(raw)
@ -17,6 +25,7 @@ export function initServerSentry() {
Sentry.init({ Sentry.init({
dsn, dsn,
tunnel: sentryTunnel(),
environment: process.env.SENTRY_ENVIRONMENT?.trim() || process.env.NODE_ENV || 'production', environment: process.env.SENTRY_ENVIRONMENT?.trim() || process.env.NODE_ENV || 'production',
release: process.env.SENTRY_RELEASE?.trim() || process.env.NEXT_PUBLIC_APP_VERSION || undefined, release: process.env.SENTRY_RELEASE?.trim() || process.env.NEXT_PUBLIC_APP_VERSION || undefined,
tracesSampleRate: tracesSampleRate(), tracesSampleRate: tracesSampleRate(),