| @@ -0,0 +1,179 @@ | |||
| 1 | + | # ── Rate limiting zones (http-context; fine here since this file is | |
| 2 | + | # included inside the http {} block via sites-enabled) ── | |
| 3 | + | #limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; | |
| 4 | + | #limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; | |
| 5 | + | server { | |
| 6 | + | listen 80; | |
| 7 | + | listen [::]:80; | |
| 8 | + | server_name p.onni.me; | |
| 9 | + | ||
| 10 | + | add_header X-Content-Type-Options nosniff always; | |
| 11 | + | add_header X-Frame-Options DENY always; | |
| 12 | + | ||
| 13 | + | access_log /var/log/nginx/p.onni.me.log combined if=$loggable; | |
| 14 | + | error_log /var/log/nginx/_error_p.onni.me.log warn; | |
| 15 | + | ||
| 16 | + | # Keep ACME challenge reachable for certbot webroot/HTTP-01 renewal | |
| 17 | + | location ^~ /.well-known/acme-challenge/ { | |
| 18 | + | root /home/www-data/p.onni.me; | |
| 19 | + | default_type "text/plain"; | |
| 20 | + | allow all; | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | return 301 https://$server_name$request_uri; | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | # ── Main HTTPS server ── | |
| 27 | + | server { | |
| 28 | + | # HTTP/2 over TCP | |
| 29 | + | listen 443 ssl; | |
| 30 | + | listen [::]:443 ssl; | |
| 31 | + | ||
| 32 | + | # HTTP/3 over QUIC/UDP | |
| 33 | + | listen 443 quic; | |
| 34 | + | listen [::]:443 quic; | |
| 35 | + | ||
| 36 | + | http2 on; | |
| 37 | + | http3 on; | |
| 38 | + | ||
| 39 | + | add_header Alt-Svc 'h3=":443"; ma=86400' always; | |
| 40 | + | ||
| 41 | + | server_name p.onni.me; | |
| 42 | + | ||
| 43 | + | root /home/www-data/p.onni.me; | |
| 44 | + | ||
| 45 | + | # SSL Certificate Configuration (Certbot managed) | |
| 46 | + | ||
| 47 | + | include /etc/letsencrypt/options-ssl-nginx.conf; | |
| 48 | + | ssl_certificate /etc/letsencrypt/live/p.onni.me/fullchain.pem; | |
| 49 | + | ssl_certificate_key /etc/letsencrypt/live/p.onni.me/privkey.pem; | |
| 50 | + | ssl_dhparam /etc/letsencrypt/dh/tau.pem; | |
| 51 | + | ||
| 52 | + | # Security headers | |
| 53 | + | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; | |
| 54 | + | add_header X-Content-Type-Options nosniff always; | |
| 55 | + | add_header X-Frame-Options SAMEORIGIN always; | |
| 56 | + | add_header X-XSS-Protection "1; mode=block" always; | |
| 57 | + | add_header X-Robots-Tag "noindex, nofollow" always; | |
| 58 | + | add_header X-Download-Options noopen always; | |
| 59 | + | add_header X-Permitted-Cross-Domain-Policies none always; | |
| 60 | + | add_header Referrer-Policy "strict-origin-when-cross-origin" always; | |
| 61 | + | ||
| 62 | + | # CSP for SnowShare (Next.js). Adjust img-src/connect-src if you enable | |
| 63 | + | # S3-compatible storage on a different origin, or add Plausible's host | |
| 64 | + | # if TELEMETRY=true and it's not self-hosted under this domain. | |
| 65 | + | add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-src 'self'; object-src 'none'; base-uri 'self';" always; | |
| 66 | + | ||
| 67 | + | # Global body size ceiling — set to your intended max upload size. | |
| 68 | + | # This must be >= whatever you configure in SnowShare's admin panel, | |
| 69 | + | # or nginx will 413 before the app ever gets to enforce its own limit. | |
| 70 | + | client_max_body_size 1G; | |
| 71 | + | client_body_timeout 300s; | |
| 72 | + | client_header_timeout 60s; | |
| 73 | + | ||
| 74 | + | proxy_connect_timeout 60s; | |
| 75 | + | proxy_send_timeout 300s; | |
| 76 | + | proxy_read_timeout 300s; | |
| 77 | + | send_timeout 60s; | |
| 78 | + | ||
| 79 | + | proxy_buffering on; | |
| 80 | + | proxy_buffer_size 4k; | |
| 81 | + | proxy_buffers 8 4k; | |
| 82 | + | proxy_busy_buffers_size 8k; | |
| 83 | + | proxy_max_temp_file_size 1024m; | |
| 84 | + | proxy_temp_file_write_size 64k; | |
| 85 | + | ||
| 86 | + | access_log /var/log/nginx/p.onni.me.log combined if=$loggable; | |
| 87 | + | error_log /var/log/nginx/_error_p.onni.me.log warn; | |
| 88 | + | ||
| 89 | + | gzip on; | |
| 90 | + | gzip_vary on; | |
| 91 | + | gzip_comp_level 6; | |
| 92 | + | gzip_min_length 1000; | |
| 93 | + | gzip_proxied any; | |
| 94 | + | gzip_types | |
| 95 | + | text/plain | |
| 96 | + | text/css | |
| 97 | + | text/xml | |
| 98 | + | text/javascript | |
| 99 | + | application/json | |
| 100 | + | application/javascript | |
| 101 | + | application/xml+rss | |
| 102 | + | application/atom+xml | |
| 103 | + | image/svg+xml; | |
| 104 | + | ||
| 105 | + | # ── Main SnowShare proxy ── | |
| 106 | + | location / { | |
| 107 | + | # limit_req zone=api burst=20 nodelay; | |
| 108 | + | ||
| 109 | + | proxy_pass http://127.0.0.1:3000; | |
| 110 | + | proxy_http_version 1.1; | |
| 111 | + | ||
| 112 | + | proxy_set_header Host $host; | |
| 113 | + | proxy_set_header X-Real-IP $remote_addr; | |
| 114 | + | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |
| 115 | + | proxy_set_header X-Forwarded-Proto $scheme; | |
| 116 | + | proxy_set_header X-Forwarded-Host $host; | |
| 117 | + | proxy_set_header X-Forwarded-Port $server_port; | |
| 118 | + | ||
| 119 | + | # Large-upload tuning: don't buffer the whole request body to disk | |
| 120 | + | # before forwarding — matters far more than response buffering for | |
| 121 | + | # a file-sharing app. Keep this in sync with whatever max size you | |
| 122 | + | # set in SnowShare's admin panel and the client_max_body_size above. | |
| 123 | + | client_max_body_size 1G; | |
| 124 | + | proxy_request_buffering off; | |
| 125 | + | proxy_buffering off; | |
| 126 | + | proxy_redirect off; | |
| 127 | + | proxy_cache_bypass $http_upgrade; | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | # Login / auth endpoints — stricter rate limiting | |
| 131 | + | location ~ ^/(api/auth|rest/login) { | |
| 132 | + | # limit_req zone=login burst=3 nodelay; | |
| 133 | + | ||
| 134 | + | proxy_pass http://127.0.0.1:3000; | |
| 135 | + | proxy_http_version 1.1; | |
| 136 | + | ||
| 137 | + | proxy_set_header Host $host; | |
| 138 | + | proxy_set_header X-Real-IP $remote_addr; | |
| 139 | + | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |
| 140 | + | proxy_set_header X-Forwarded-Proto $scheme; | |
| 141 | + | proxy_set_header X-Forwarded-Host $host; | |
| 142 | + | ||
| 143 | + | proxy_redirect off; | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | # Static assets with caching | |
| 147 | + | location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map)$ { | |
| 148 | + | proxy_pass http://127.0.0.1:3000; | |
| 149 | + | ||
| 150 | + | proxy_set_header Host $host; | |
| 151 | + | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |
| 152 | + | proxy_set_header X-Forwarded-Proto $scheme; | |
| 153 | + | ||
| 154 | + | expires 30d; | |
| 155 | + | add_header Cache-Control "public, no-transform"; | |
| 156 | + | add_header Vary Accept-Encoding; | |
| 157 | + | add_header Access-Control-Allow-Origin "*"; | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | # Health check | |
| 161 | + | location = /healthz { | |
| 162 | + | access_log off; | |
| 163 | + | return 200 "healthy\n"; | |
| 164 | + | add_header Content-Type text/plain; | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | # Block access to dotfiles / VCS metadata — NOT /admin, which is a | |
| 168 | + | # legitimate SnowShare route. Trim this list further if you don't | |
| 169 | + | # run PHP or WordPress anywhere else under this vhost. | |
| 170 | + | location ~ /\.(ht|env|git|svn) { | |
| 171 | + | deny all; | |
| 172 | + | return 404; | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | location ~ ^/(wp-admin|phpmyadmin) { | |
| 176 | + | deny all; | |
| 177 | + | return 404; | |
| 178 | + | } | |
| 179 | + | } | |
meson revised this gist 3 weeks ago · 375fe7c
1 file changed, 179 insertions