diff --git a/infra/nginx/sites-enabled/pw-api.conf b/infra/nginx/sites-enabled/pw-api.conf new file mode 100644 index 0000000..03a98cf --- /dev/null +++ b/infra/nginx/sites-enabled/pw-api.conf @@ -0,0 +1,313 @@ +# HTTPS config for api.performancewest.net +# Restricted: only specific paths are publicly accessible. +# Admin/internal paths require trusted IP. + +limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; + +geo $api_trusted { + default 0; + 127.0.0.1/32 1; + 172.16.0.0/12 1; + 10.0.0.0/8 1; + 207.174.124.71/32 1; + 76.228.206.147/32 1; +} + +server { + listen 80; + server_name api.performancewest.net; + location /.well-known/acme-challenge/ { root /var/www/certbot; } + location / { return 301 https://api.performancewest.net$request_uri; } +} + +server { + listen 443 ssl; + http2 on; + server_name api.performancewest.net; + + ssl_certificate /etc/letsencrypt/live/api.performancewest.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/api.performancewest.net/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + client_max_body_size 50m; + + # Shared proxy settings + set $api_backend http://127.0.0.1:3001; + + # ── Webhooks: fully public (Stripe, PayPal, SHKeeper) ── + location ^~ /api/v1/webhooks/ { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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; + } + + # ── Public API paths (browser frontend needs these) ── + location ^~ /api/v1/fcc/ { + limit_req zone=api_limit burst=20 nodelay; + proxy_pass $api_backend; + 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_connect_timeout 10s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location ^~ /api/v1/id-upload/ { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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; + client_max_body_size 20M; + } + + location ^~ /api/v1/dot/ { + limit_req zone=api_limit burst=20 nodelay; + proxy_pass $api_backend; + 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_connect_timeout 10s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location ^~ /api/v1/discount/ { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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 ^~ /api/v1/auth/ { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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 ^~ /api/v1/subscribe { + limit_req zone=api_limit burst=5 nodelay; + proxy_pass $api_backend; + 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 ^~ /api/v1/tickets { + limit_req zone=api_limit burst=5 nodelay; + proxy_pass $api_backend; + 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 ^~ /api/v1/checkout/ { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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_send_timeout 60s; + proxy_read_timeout 60s; + } + + location ^~ /api/v1/compliance-orders { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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_send_timeout 60s; + proxy_read_timeout 60s; + } + + location ^~ /api/v1/portal { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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 ^~ /api/v1/canada-crtc { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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_send_timeout 60s; + proxy_read_timeout 60s; + } + + location ^~ /api/v1/amb/ { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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 ^~ /api/v1/formations { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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_send_timeout 60s; + proxy_read_timeout 60s; + } + + location ^~ /api/v1/paypal { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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 ^~ /api/v1/entities/ { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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 ^~ /api/v1/identity { + limit_req zone=api_limit burst=5 nodelay; + proxy_pass $api_backend; + 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; + } + + # ── Public site routes (browser-facing tools/flows) ── + # Added: these power public lead tools + order flows and MUST be reachable by + # untrusted visitors. Without an explicit block they fell through to the + # trusted-IP-only catch-all and returned 403, silently breaking: + # npi -> Healthcare "Free NPI Compliance Check" (top of the HC funnel; the + # fetch failed CORS/403 so the tool never rendered results -> 0 HC sales) + # cdr/icc/corp/foreign-qualification/lnpa-regions -> telecom/corporate tools. + location ^~ /api/v1/npi/ { + limit_req zone=api_limit burst=20 nodelay; + proxy_pass $api_backend; + 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 ^~ /api/v1/cdr/ { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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 ^~ /api/v1/icc/ { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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 ^~ /api/v1/corp/ { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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 ^~ /api/v1/foreign-qualification/ { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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 ^~ /api/v1/lnpa-regions { + limit_req zone=api_limit burst=10 nodelay; + proxy_pass $api_backend; + 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; + } + + # ── Everything else: trusted IPs only ── + location / { + if ($api_trusted = 0) { + return 403; + } + limit_req zone=api_limit burst=20 nodelay; + proxy_pass $api_backend; + 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_connect_timeout 10s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location /.well-known/acme-challenge/ { root /var/www/certbot; } +}