nginx-cf.conf
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2
feat: add repair-commit wire endpoint (API parity with repa…
Opus 4.8
minor
⚠ breaking
1 day ago
| 1 | # /etc/nginx/sites-available/musehub |
| 2 | # |
| 3 | # Cloudflare Origin Certificate configuration. |
| 4 | # Cloudflare terminates SSL at the edge. This nginx instance accepts HTTPS |
| 5 | # connections using a Cloudflare Origin Certificate. |
| 6 | # |
| 7 | # SSL mode in Cloudflare dashboard MUST be set to "Full (Strict)". |
| 8 | # No Certbot or Let's Encrypt required — the Origin Certificate is valid for 15 years. |
| 9 | # |
| 10 | # IP restriction is enforced at the EC2 security group level (inbound port 443 |
| 11 | # restricted to Cloudflare IP ranges). Do not duplicate that logic here — |
| 12 | # the real_ip module replaces $remote_addr with the true client IP before |
| 13 | # allow/deny runs, which would incorrectly block legitimate Cloudflare traffic. |
| 14 | # |
| 15 | # To generate the Origin Certificate: |
| 16 | # Cloudflare Dashboard → <domain> → SSL/TLS → Origin Server → Create Certificate |
| 17 | # Choose "Generate private key and CSR with Cloudflare" → RSA (2048) → 15 years |
| 18 | # Save certificate → /etc/ssl/cloudflare/origin.pem |
| 19 | # Save private key → /etc/ssl/cloudflare/origin.key |
| 20 | # chmod 640 /etc/ssl/cloudflare/origin.key |
| 21 | |
| 22 | # Per-IP rate limiting zones — defined at http context level. |
| 23 | # Wire endpoints are exempt (MSign auth-gated). Healthz is exempt (fast-path). |
| 24 | # 'api' — general unauthenticated surface: 60 req/min with burst headroom |
| 25 | # 'auth' — tighter zone reserved for future auth endpoints: 10 req/min |
| 26 | limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m; |
| 27 | limit_req_zone $binary_remote_addr zone=auth:10m rate=10r/m; |
| 28 | |
| 29 | # Restore the real client IP from the Cloudflare connecting-IP header. |
| 30 | # Without this, every request appears to come from a Cloudflare edge node. |
| 31 | # Cloudflare IP ranges: https://www.cloudflare.com/ips/ |
| 32 | real_ip_header CF-Connecting-IP; |
| 33 | real_ip_recursive on; |
| 34 | |
| 35 | # Cloudflare IPv4 ranges |
| 36 | set_real_ip_from 173.245.48.0/20; |
| 37 | set_real_ip_from 103.21.244.0/22; |
| 38 | set_real_ip_from 103.22.200.0/22; |
| 39 | set_real_ip_from 103.31.4.0/22; |
| 40 | set_real_ip_from 141.101.64.0/18; |
| 41 | set_real_ip_from 108.162.192.0/18; |
| 42 | set_real_ip_from 190.93.240.0/20; |
| 43 | set_real_ip_from 188.114.96.0/20; |
| 44 | set_real_ip_from 197.234.240.0/22; |
| 45 | set_real_ip_from 198.41.128.0/17; |
| 46 | set_real_ip_from 162.158.0.0/15; |
| 47 | set_real_ip_from 104.16.0.0/13; |
| 48 | set_real_ip_from 104.24.0.0/14; |
| 49 | set_real_ip_from 172.64.0.0/13; |
| 50 | set_real_ip_from 131.0.72.0/22; |
| 51 | |
| 52 | # Cloudflare IPv6 ranges |
| 53 | set_real_ip_from 2400:cb00::/32; |
| 54 | set_real_ip_from 2606:4700::/32; |
| 55 | set_real_ip_from 2803:f800::/32; |
| 56 | set_real_ip_from 2405:b500::/32; |
| 57 | set_real_ip_from 2405:8100::/32; |
| 58 | set_real_ip_from 2a06:98c0::/29; |
| 59 | set_real_ip_from 2c0f:f248::/32; |
| 60 | |
| 61 | # Blue-green upstream: deploy.sh rewrites /etc/nginx/musehub-active-port |
| 62 | # and runs `nginx -s reload` to switch slots atomically. |
| 63 | # |
| 64 | # keepalive 32: nginx keeps up to 32 idle connections to uvicorn open, |
| 65 | # eliminating the TCP handshake cost on every request. |
| 66 | # keepalive_requests 1000: rotate the connection after 1000 requests so |
| 67 | # stale file descriptors don't accumulate. |
| 68 | # keepalive_timeout 60s: evict idle keepalive connections after 60s of |
| 69 | # disuse — matches uvicorn's own idle timeout. |
| 70 | upstream musehub_backend { |
| 71 | include /etc/nginx/musehub-active-port; |
| 72 | keepalive 32; |
| 73 | keepalive_requests 1000; |
| 74 | keepalive_timeout 60s; |
| 75 | } |
| 76 | |
| 77 | # Redirect all plain-HTTP traffic to HTTPS — 301 (permanent, cacheable). |
| 78 | # In prod, Cloudflare enforces HTTPS at the edge, so this only fires for |
| 79 | # traffic that bypasses Cloudflare (e.g., direct-to-origin access during |
| 80 | # ops or monitoring). Belt-and-suspenders. |
| 81 | server { |
| 82 | listen 80; |
| 83 | listen [::]:80; |
| 84 | server_name DOMAIN_PLACEHOLDER; |
| 85 | return 301 https://$host$request_uri; |
| 86 | } |
| 87 | |
| 88 | server { |
| 89 | listen 443 ssl http2; |
| 90 | listen [::]:443 ssl http2; |
| 91 | server_name DOMAIN_PLACEHOLDER; |
| 92 | |
| 93 | ssl_certificate /etc/ssl/cloudflare/origin.pem; |
| 94 | ssl_certificate_key /etc/ssl/cloudflare/origin.key; |
| 95 | |
| 96 | # Enforce TLS 1.2 minimum — disable TLS 1.0 and TLS 1.1 (both deprecated). |
| 97 | # TLS 1.3 is preferred; 1.2 retained for compatibility with older clients. |
| 98 | ssl_protocols TLSv1.2 TLSv1.3; |
| 99 | ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256; |
| 100 | ssl_prefer_server_ciphers off; # TLS 1.3 ignores this; keep off for 1.2 forward secrecy |
| 101 | |
| 102 | client_max_body_size 500m; |
| 103 | |
| 104 | gzip on; |
| 105 | gzip_comp_level 5; |
| 106 | gzip_vary on; |
| 107 | # Binary types (msgpack) are excluded: already compressed, gzip adds CPU with no size gain. |
| 108 | gzip_types text/plain text/css text/javascript application/javascript |
| 109 | application/json; |
| 110 | |
| 111 | # Security headers — applied to all responses from this server block. |
| 112 | # 'always' ensures they are sent on error responses (4xx, 5xx) too. |
| 113 | # Wire location blocks that define their own add_header (MCP, SSE) break |
| 114 | # inheritance intentionally — programmatic clients don't need these. |
| 115 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; |
| 116 | add_header X-Content-Type-Options "nosniff" always; |
| 117 | add_header X-Frame-Options "SAMEORIGIN" always; |
| 118 | add_header Referrer-Policy "strict-origin-when-cross-origin" always; |
| 119 | |
| 120 | # ── Healthz — dedicated fast-path, short timeout, no interference ──────────── |
| 121 | location = /healthz { |
| 122 | proxy_pass http://musehub_backend; |
| 123 | proxy_http_version 1.1; |
| 124 | proxy_set_header Connection ""; |
| 125 | proxy_set_header Host $host; |
| 126 | proxy_set_header X-Real-IP $remote_addr; |
| 127 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 128 | proxy_set_header X-Forwarded-Proto $scheme; |
| 129 | proxy_read_timeout 5s; |
| 130 | proxy_send_timeout 5s; |
| 131 | } |
| 132 | |
| 133 | # ── MCP — bidirectional Streamable HTTP, long-lived, never buffer ────────── |
| 134 | location ~ ^/mcp(/|$) { |
| 135 | proxy_pass http://musehub_backend; |
| 136 | proxy_http_version 1.1; |
| 137 | proxy_set_header Connection ""; |
| 138 | proxy_set_header Host $host; |
| 139 | proxy_set_header X-Real-IP $remote_addr; |
| 140 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 141 | proxy_set_header X-Forwarded-Proto $scheme; |
| 142 | proxy_read_timeout 3600s; |
| 143 | proxy_send_timeout 3600s; |
| 144 | proxy_request_buffering off; |
| 145 | proxy_buffering off; |
| 146 | add_header X-Accel-Buffering no always; |
| 147 | } |
| 148 | |
| 149 | # ── SSE — Server-Sent Events (social feed), long-lived, never buffer ─────── |
| 150 | location ~ ^/api/social/[^/]+/stream$ { |
| 151 | proxy_pass http://musehub_backend; |
| 152 | proxy_http_version 1.1; |
| 153 | proxy_set_header Connection ""; |
| 154 | proxy_set_header Host $host; |
| 155 | proxy_set_header X-Real-IP $remote_addr; |
| 156 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 157 | proxy_set_header X-Forwarded-Proto $scheme; |
| 158 | proxy_read_timeout 3600s; |
| 159 | proxy_send_timeout 3600s; |
| 160 | proxy_request_buffering off; |
| 161 | proxy_buffering off; |
| 162 | add_header X-Accel-Buffering no always; |
| 163 | } |
| 164 | |
| 165 | # ── Wire push — large uploads, never buffer ─────────────────────────────── |
| 166 | # /push/mpack-presign — negotiate S3 presigned upload URL |
| 167 | # /push/unpack-mpack — trigger S3→uvicorn unpack |
| 168 | location ~ ^/[^/]+/[^/]+/push(/mpack-presign|/unpack-mpack)?$ { |
| 169 | proxy_pass http://musehub_backend; |
| 170 | proxy_http_version 1.1; |
| 171 | proxy_set_header Connection ""; |
| 172 | proxy_set_header Host $host; |
| 173 | proxy_set_header X-Real-IP $remote_addr; |
| 174 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 175 | proxy_set_header X-Forwarded-Proto $scheme; |
| 176 | proxy_read_timeout 300s; |
| 177 | proxy_send_timeout 300s; |
| 178 | proxy_request_buffering off; |
| 179 | proxy_buffering off; |
| 180 | client_max_body_size 0; |
| 181 | gzip off; |
| 182 | } |
| 183 | |
| 184 | # ── Wire fetch — responses may be large, never buffer ───────────────────── |
| 185 | # /fetch/mpack — mpack download (inline bytes or presigned S3 URL) |
| 186 | # /fetch/objects — raw object bytes |
| 187 | # /fetch/presign — negotiate S3 presigned download URL |
| 188 | location ~ ^/[^/]+/[^/]+/fetch(/mpack|/objects|/presign)?$ { |
| 189 | proxy_pass http://musehub_backend; |
| 190 | proxy_http_version 1.1; |
| 191 | proxy_set_header Connection ""; |
| 192 | proxy_set_header Host $host; |
| 193 | proxy_set_header X-Real-IP $remote_addr; |
| 194 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 195 | proxy_set_header X-Forwarded-Proto $scheme; |
| 196 | proxy_read_timeout 300s; |
| 197 | proxy_send_timeout 300s; |
| 198 | proxy_request_buffering off; |
| 199 | proxy_buffering off; |
| 200 | client_max_body_size 0; |
| 201 | gzip off; |
| 202 | } |
| 203 | |
| 204 | # ── Wire misc — short-lived wire ops and raw object downloads ───────────── |
| 205 | # These are authenticated wire endpoints that need more than 60s: refs, |
| 206 | # repair, tags, releases, branches, coord, and the raw object download path /o/. |
| 207 | location ~ ^(/[^/]+/[^/]+/(refs|repair-object|repair-snapshot|repair-commit|tags|releases|branches|coord)|/o/) { |
| 208 | proxy_pass http://musehub_backend; |
| 209 | proxy_http_version 1.1; |
| 210 | proxy_set_header Connection ""; |
| 211 | proxy_set_header Host $host; |
| 212 | proxy_set_header X-Real-IP $remote_addr; |
| 213 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 214 | proxy_set_header X-Forwarded-Proto $scheme; |
| 215 | proxy_read_timeout 120s; |
| 216 | proxy_send_timeout 120s; |
| 217 | gzip off; |
| 218 | } |
| 219 | |
| 220 | # ── Default catch-all — UI pages and JSON API ───────────────────────────── |
| 221 | # All /api/*, /{owner}/{repo_slug} UI routes, /musehub/*, /explore, etc. |
| 222 | # Standard request/response, small payloads, 60s is ample. |
| 223 | # Rate-limited: unauthenticated endpoints here are the primary abuse surface. |
| 224 | # burst=20 nodelay: absorb short bursts without queuing; excess → 429 immediately. |
| 225 | location / { |
| 226 | limit_req zone=api burst=20 nodelay; |
| 227 | proxy_pass http://musehub_backend; |
| 228 | proxy_http_version 1.1; |
| 229 | proxy_set_header Connection ""; |
| 230 | proxy_set_header Host $host; |
| 231 | proxy_set_header X-Real-IP $remote_addr; |
| 232 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 233 | proxy_set_header X-Forwarded-Proto $scheme; |
| 234 | proxy_read_timeout 60s; |
| 235 | } |
| 236 | } |
File History
1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2
feat: add repair-commit wire endpoint (API parity with repa…
Opus 4.8
minor
⚠
1 day ago