# ── Rate limiting zones (http-context; fine here since this file is # included inside the http {} block via sites-enabled) ── #limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; #limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; server { listen 80; listen [::]:80; server_name p.onni.me; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; access_log /var/log/nginx/p.onni.me.log combined if=$loggable; error_log /var/log/nginx/_error_p.onni.me.log warn; # Keep ACME challenge reachable for certbot webroot/HTTP-01 renewal location ^~ /.well-known/acme-challenge/ { root /home/www-data/p.onni.me; default_type "text/plain"; allow all; } return 301 https://$server_name$request_uri; } # ── Main HTTPS server ── server { # HTTP/2 over TCP listen 443 ssl; listen [::]:443 ssl; # HTTP/3 over QUIC/UDP listen 443 quic; listen [::]:443 quic; http2 on; http3 on; add_header Alt-Svc 'h3=":443"; ma=86400' always; server_name p.onni.me; root /home/www-data/p.onni.me; # SSL Certificate Configuration (Certbot managed) include /etc/letsencrypt/options-ssl-nginx.conf; ssl_certificate /etc/letsencrypt/live/p.onni.me/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/p.onni.me/privkey.pem; ssl_dhparam /etc/letsencrypt/dh/tau.pem; # Security headers add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Robots-Tag "noindex, nofollow" always; add_header X-Download-Options noopen always; add_header X-Permitted-Cross-Domain-Policies none always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # CSP for SnowShare (Next.js). Adjust img-src/connect-src if you enable # S3-compatible storage on a different origin, or add Plausible's host # if TELEMETRY=true and it's not self-hosted under this domain. 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; # Global body size ceiling — set to your intended max upload size. # This must be >= whatever you configure in SnowShare's admin panel, # or nginx will 413 before the app ever gets to enforce its own limit. client_max_body_size 1G; client_body_timeout 300s; client_header_timeout 60s; proxy_connect_timeout 60s; proxy_send_timeout 300s; proxy_read_timeout 300s; send_timeout 60s; proxy_buffering on; proxy_buffer_size 4k; proxy_buffers 8 4k; proxy_busy_buffers_size 8k; proxy_max_temp_file_size 1024m; proxy_temp_file_write_size 64k; access_log /var/log/nginx/p.onni.me.log combined if=$loggable; error_log /var/log/nginx/_error_p.onni.me.log warn; gzip on; gzip_vary on; gzip_comp_level 6; gzip_min_length 1000; gzip_proxied any; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/atom+xml image/svg+xml; # ── Main SnowShare proxy ── location / { # limit_req zone=api burst=20 nodelay; proxy_pass http://127.0.0.1:3000; 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_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; # Large-upload tuning: don't buffer the whole request body to disk # before forwarding — matters far more than response buffering for # a file-sharing app. Keep this in sync with whatever max size you # set in SnowShare's admin panel and the client_max_body_size above. client_max_body_size 1G; proxy_request_buffering off; proxy_buffering off; proxy_redirect off; proxy_cache_bypass $http_upgrade; } # Login / auth endpoints — stricter rate limiting location ~ ^/(api/auth|rest/login) { # limit_req zone=login burst=3 nodelay; proxy_pass http://127.0.0.1:3000; 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_set_header X-Forwarded-Host $host; proxy_redirect off; } # Static assets with caching location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map)$ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; expires 30d; add_header Cache-Control "public, no-transform"; add_header Vary Accept-Encoding; add_header Access-Control-Allow-Origin "*"; } # Health check location = /healthz { access_log off; return 200 "healthy\n"; add_header Content-Type text/plain; } # Block access to dotfiles / VCS metadata — NOT /admin, which is a # legitimate SnowShare route. Trim this list further if you don't # run PHP or WordPress anywhere else under this vhost. location ~ /\.(ht|env|git|svn) { deny all; return 404; } location ~ ^/(wp-admin|phpmyadmin) { deny all; return 404; } }