nginx: ship security baseline, reference vhost, and tighter cache
- Add nginx/security-headers.conf — server_tokens off, HSTS (1y + preload), X-Content-Type-Options, X-Frame-Options DENY, Referrer-Policy, Permissions-Policy, and a usage-scoped CSP. CSP ships in Report-Only mode; promote to enforcing once the report stream is clean for a week. CSP allowlists are derived from actual usage (cdn.jsdelivr.net for KaTeX/Vega, *.basemaps.cartocdn.com for Leaflet tiles); 'unsafe-inline' and 'unsafe-eval' are documented inline. - Add nginx/vhost.conf.example — reference vhost showing the canonical include order. The live vhost on the VPS remains the source of truth; this file documents the structure so the VPS config can be reproduced or audited from the repo. - Shorten unfingerprinted CSS/JS cache from 24h to 1h. Bug fixes ship to warm clients within an hour; if assets are ever fingerprinted, this can move to immutable. - Refresh README repo layout — add nginx/ entry, drop stale paper/ and spec.md references that never existed in the working tree. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fd84b7e6d2
commit
87819501a5
|
|
@ -67,8 +67,10 @@ the VPS rsync target consumed by `make deploy`. Never commit it.
|
|||
- `tools/` — Python tooling (embeddings, importers) and shell scripts.
|
||||
- `data/` — generated and source data (commonplace.yaml, annotations.json,
|
||||
bibliographies, similar-links.json).
|
||||
- `paper/` — LaTeX source for in-progress academic papers.
|
||||
- `spec.md` — full architectural notes and design intent.
|
||||
- `nginx/` — vhost snippets shipped to the VPS (`security-headers.conf`,
|
||||
`static-assets.conf`, `popup-proxy.conf`). The live vhost on the VPS
|
||||
is the source of truth; see `nginx/vhost.conf.example` for the
|
||||
canonical structure and the include order these snippets expect.
|
||||
|
||||
## Architecture pointers
|
||||
|
||||
|
|
@ -79,8 +81,6 @@ the VPS rsync target consumed by `make deploy`. Never commit it.
|
|||
- `build/Filters/Images.hs` does WebP `<picture>` wrapping; requires
|
||||
the `.webp` companions produced by `tools/convert-images.sh`.
|
||||
|
||||
For deeper architectural detail, see `spec.md`.
|
||||
|
||||
## License
|
||||
|
||||
See `LICENSE`.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
# security-headers.conf — security baseline for the levineuwirth.org vhost.
|
||||
#
|
||||
# Place at /etc/nginx/snippets/security-headers.conf and `include` it
|
||||
# inside the server { } block of the vhost, alongside the other
|
||||
# snippets shipped from this repo:
|
||||
#
|
||||
# server {
|
||||
# server_name levineuwirth.org;
|
||||
# root /var/www/levineuwirth.org;
|
||||
# ...
|
||||
# include snippets/security-headers.conf;
|
||||
# include snippets/static-assets.conf;
|
||||
# include snippets/popup-proxy.conf;
|
||||
# }
|
||||
|
||||
# Hide the nginx version from error pages and the Server header.
|
||||
server_tokens off;
|
||||
|
||||
# HSTS — one year, with subdomains, preload-eligible. Only safe to
|
||||
# enable once HTTPS is the only listener (the :80 vhost should already
|
||||
# 301 → https). Once preload is requested via hstspreload.org, the
|
||||
# max-age cannot be lowered without removing from the list manually.
|
||||
add_header Strict-Transport-Security
|
||||
"max-age=31536000; includeSubDomains; preload" always;
|
||||
|
||||
# Block MIME-sniffing — required for safe text/plain and svg responses.
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
|
||||
# Defense against clickjacking. Modern browsers honor CSP frame-ancestors
|
||||
# (set below); X-Frame-Options is kept for legacy clients.
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
|
||||
# Strip the Referer header to "scheme + host" on cross-origin requests.
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Default-deny powerful APIs. interest-cohort opts out of FLoC/Topics.
|
||||
add_header Permissions-Policy
|
||||
"camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
|
||||
|
||||
# Content Security Policy. Shipped in Report-Only mode initially —
|
||||
# promote to enforcing (strip the "-Report-Only" suffix) once the
|
||||
# report stream has been clean for a week.
|
||||
#
|
||||
# External origins justified inline:
|
||||
# cdn.jsdelivr.net KaTeX CSS + JS, Vega / Vega-Lite / Vega-Embed
|
||||
# *.basemaps.cartocdn.com Leaflet basemap tiles (photography map only)
|
||||
#
|
||||
# Why 'unsafe-inline' on style:
|
||||
# - photography.html emits <span style="background:$swatch$"> for
|
||||
# palette swatches (data-driven, can't be hashed).
|
||||
# - KaTeX adds inline styles when rendering math at runtime.
|
||||
#
|
||||
# Why 'unsafe-eval' on script:
|
||||
# - vega-embed compiles Vega-Lite specs at runtime via new Function().
|
||||
# Removing this would require pre-compiling specs at build time.
|
||||
#
|
||||
# To collect violation reports, set up a `report-uri` endpoint and add
|
||||
# `report-uri /csp-report;` (and/or `report-to <group>;`) below.
|
||||
add_header Content-Security-Policy-Report-Only
|
||||
"default-src 'self'; \
|
||||
script-src 'self' 'unsafe-eval' https://cdn.jsdelivr.net; \
|
||||
style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; \
|
||||
img-src 'self' data: https://*.basemaps.cartocdn.com; \
|
||||
font-src 'self' data:; \
|
||||
connect-src 'self'; \
|
||||
frame-ancestors 'none'; \
|
||||
base-uri 'self'; \
|
||||
form-action 'self'; \
|
||||
object-src 'none'; \
|
||||
upgrade-insecure-requests" always;
|
||||
|
|
@ -70,9 +70,10 @@ location ^~ /models/ {
|
|||
}
|
||||
|
||||
# Per-extension caching for assets that live alongside HTML. CSS and JS
|
||||
# in this repo are not fingerprinted, so a 1-day cache with must-revalidate
|
||||
# keeps them responsive to deploys without forcing a fetch on every page.
|
||||
# in this repo are not fingerprinted, so a 1-hour cache with must-revalidate
|
||||
# keeps them responsive to deploys (~1h staleness window for warm clients)
|
||||
# without forcing a fetch on every page navigation.
|
||||
location ~* \.(?:css|js|mjs|woff2?|svg|webp|png|jpg|jpeg|ico)$ {
|
||||
add_header Cache-Control "public, max-age=86400, must-revalidate" always;
|
||||
add_header Cache-Control "public, max-age=3600, must-revalidate" always;
|
||||
access_log off;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
# vhost.conf.example — reference vhost for levineuwirth.org.
|
||||
#
|
||||
# The live vhost on the VPS is the source of truth and lives at
|
||||
# /etc/nginx/sites-available/levineuwirth.conf. `make deploy` does not
|
||||
# touch the vhost — it only rsyncs _site/ to the document root. This
|
||||
# file exists so the canonical structure (which snippets to include,
|
||||
# in what order) is documented in the repo.
|
||||
#
|
||||
# To adopt: copy this file to /etc/nginx/sites-available/levineuwirth.conf
|
||||
# on the VPS, fill in the certificate paths, and `nginx -t && systemctl
|
||||
# reload nginx`. The three snippets it includes ship from this repo's
|
||||
# nginx/ directory and should be installed under /etc/nginx/snippets/.
|
||||
|
||||
# ── http { } scope ──────────────────────────────────────────────────
|
||||
# popup-proxy.conf consumes a `proxy_cache_path` defined in http { }.
|
||||
# Place this directive in nginx.conf or a conf.d/ file:
|
||||
#
|
||||
# proxy_cache_path /var/cache/nginx/popup-proxy
|
||||
# levels=1:2 keys_zone=popup_proxy:16m
|
||||
# max_size=512m inactive=60d use_temp_path=off;
|
||||
#
|
||||
# popup-proxy.conf also defines a `limit_req_zone` for PubMed; place
|
||||
# its companion zone definition in http { } as well:
|
||||
#
|
||||
# limit_req_zone $binary_remote_addr zone=pubmed:1m rate=3r/s;
|
||||
|
||||
# ── HTTPS server ────────────────────────────────────────────────────
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
listen [::]:443 ssl;
|
||||
|
||||
server_name levineuwirth.org;
|
||||
root /var/www/levineuwirth.org;
|
||||
index index.html;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/levineuwirth.org/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/levineuwirth.org/privkey.pem;
|
||||
|
||||
# Order matters: security-headers first so add_header directives
|
||||
# propagate into the locations defined by the other snippets.
|
||||
include snippets/security-headers.conf;
|
||||
include snippets/static-assets.conf;
|
||||
include snippets/popup-proxy.conf;
|
||||
|
||||
# Static-site fallback. Pretty URLs first (foo/index.html, foo.html),
|
||||
# then 404.
|
||||
location / {
|
||||
try_files $uri $uri/index.html $uri.html =404;
|
||||
}
|
||||
|
||||
# Custom 404. The build emits _site/404.html.
|
||||
error_page 404 /404.html;
|
||||
}
|
||||
|
||||
# ── HTTP → HTTPS redirect ───────────────────────────────────────────
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name levineuwirth.org;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
Loading…
Reference in New Issue