Files
dtf-system/deploy/nginx.conf.template
Cauê Faleiros da903db32a
All checks were successful
Build and deploy / Validate source (push) Successful in 5s
Build and deploy / Integration suite on a real stack (push) Successful in 1m10s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images and notify Portainer (push) Successful in 1m43s
feat: serve pdf.js from this origin instead of a CDN
The Site pulled pdf.js 3.11.174 from cdnjs with no integrity attribute, and the
policy trusted the whole of cdnjs.cloudflare.com for both script-src and
worker-src. Anything that host served would have executed, and a customer
measuring a PDF sheet depended on it being reachable.

Vendor both files instead of pinning a hash: it removes the dependency rather
than constraining it, and lets the policy name only 'self'. Provenance and
SHA-256 digests are recorded in local/static/vendor/README.md, verified on
download against the SRI digests cdnjs publishes for that release.

cdnjs is now absent from script-src, worker-src and connect-src in both gateway
templates. Workers are 'self' plus blob:, which the Site needs for the worker it
constructs itself.

Verified in a browser against the running stack: pdf.js loads from /vendor/, the
blob worker starts, and a real seven-page PDF parses with no CSP violation. Both
browser suites and the full integration suite pass.

The version is deliberately unchanged. 3.11.174 is old, but its known eval path
is already closed by isEvalSupported:false, and upgrading is an API change that
needs its own testing rather than riding along with this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 13:56:02 -03:00

56 lines
2.8 KiB
Plaintext

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s;
server {
listen 8080;
server_name ${PUBLIC_HOST};
if ($host != ${PUBLIC_HOST}) { return 400; }
# Resolve through Docker's embedded DNS at request time. This prevents
# Nginx from exiting during a Swarm rollout when the API task is briefly
# unavailable or still creating its database schema.
resolver 127.0.0.11 ipv6=off valid=10s;
set $api_upstream api:8000;
# This gateway sits behind the host's reverse proxy, so $remote_addr is that
# proxy, not the customer. Recover the real address from the header it sets,
# and only when the connection comes from a private network: a request that
# reaches the published port directly from the internet is not trusted, so
# its X-Forwarded-For is ignored and $remote_addr stays the actual peer.
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
root /usr/share/nginx/html;
index ${WEB_INDEX};
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy no-referrer always;
add_header X-Frame-Options DENY always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' @SCRIPT_HASHES@; script-src-attr 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://cdn.vnda.com.br; connect-src 'self' ${S3_PUBLIC_ENDPOINT} https://cdn.vnda.com.br; worker-src 'self' blob:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'" always;
location = /health { access_log off; return 200 'ok'; }
location /api/ {
limit_req zone=api_limit burst=100 nodelay;
limit_req_status 429;
proxy_pass http://$api_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
# Overwrite, never append: $proxy_add_x_forwarded_for keeps any header the
# client sent, and the leftmost value would then be attacker-controlled.
# After real_ip above, $remote_addr is the customer even behind the proxy.
proxy_set_header X-Forwarded-For $remote_addr;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
client_max_body_size 2m;
}
# Always revalidate HTML/JS/CSS after a deployment. Without this, a browser
# can pair a new Kanban page with a cached older script after a rollout.
location / {
expires -1;
try_files $uri $uri/ =404;
}
}