mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
#1115 raised HSTS to one year but set no X-Content-Type-Options, Referrer-Policy, Permissions-Policy or HSTS includeSubDomains/preload, because none of them is safe globally: nosniff lets ORB block no-cors responses typed text/html, includeSubDomains on the apex covers hosts the platform does not serve, and a server-level add_header is dropped in every location that sets its own. WEBSERVER_SECURITY_HEADERS (group_vars/all/04_webserver.yml) now holds the platform values, all opt-in: nosniff false, no Referrer-Policy, no Permissions-Policy, HSTS max-age one year without includeSubDomains or preload. A role overrides any key under security_headers in its meta/server.yml, an inventory under applications.<role>.server.security_headers. The new security_headers filter merges both, rejects unknown keys, wrong types, unknown Referrer-Policy tokens, non-printable header values and preload without includeSubDomains or below a one-year max-age, and composes the HSTS value. - nosniff, Referrer-Policy and Permissions-Policy are set in the Lua header filter of content_security_policy.conf.j2, next to CSP, so a location's own add_header cannot drop them. Referrer-Policy is only set when the upstream sends none; Permissions-Policy only on HTML documents. - HSTS stays the single server-level add_header in nginx_ssl_header.j2, now with the per-app value. A second emitter in Lua would duplicate the header. It stays inside the TLS branch, which onion vhosts never enter, so an onion vhost gets no HSTS even when its app opts in. No role opts in yet, so rendered vhosts are unchanged. Closes #1123 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
41 lines
1.8 KiB
Django/Jinja
41 lines
1.8 KiB
Django/Jinja
# ===== Content Security Policy: only for documents and workers (no locations needed) =====
|
|
|
|
# 1) Define your CSP once (Jinja: escape double quotes to be safe)
|
|
set $csp "{{ lookup('applications') | build_csp_header(application_id, lookup('domains'), lookup('tls', application_id, 'protocols.web'), (webserver_csp_whitelist_extra_by_app | default({})).get(application_id, {}), (webserver_csp_hashes_extra_by_app | default({})).get(application_id, {}), DOMAIN_PRIMARY) | replace('\"','\\\"') }}";
|
|
|
|
# 2) Send CSP ONLY for document responses; also for workers via Sec-Fetch-Dest
|
|
header_filter_by_lua_block {
|
|
local ct = ngx.header.content_type or ngx.header["Content-Type"] or ""
|
|
local dest = ngx.var.http_sec_fetch_dest or ""
|
|
|
|
local lct = ct:lower()
|
|
local is_html = lct:find("^text/html") or lct:find("^application/xhtml+xml")
|
|
local is_worker = (dest == "worker") or (dest == "serviceworker")
|
|
|
|
if is_html or is_worker then
|
|
ngx.header["Content-Security-Policy"] = ngx.var.csp
|
|
else
|
|
ngx.header["Content-Security-Policy"] = nil
|
|
ngx.header["Content-Security-Policy-Report-Only"] = nil
|
|
end
|
|
|
|
{% include 'roles/sys-svc-proxy/templates/headers/security_headers.lua.j2' %}
|
|
|
|
-- If you'll modify the body later, drop Content-Length on HTML
|
|
if is_html then
|
|
ngx.ctx.is_html = true
|
|
ngx.header.content_length = nil
|
|
else
|
|
ngx.ctx.is_html = false
|
|
{% if (lookup('config', application_id, 'services.tor.enabled', False) | bool) and (lookup('applications') | external_asset_origins(application_id, DOMAIN_PRIMARY)) %}
|
|
if lct:find("^text/css") and (ngx.var.http_host or ""):sub(-6) == ".onion" then
|
|
ngx.ctx.is_css = true
|
|
ngx.header.content_length = nil
|
|
end
|
|
{% endif %}
|
|
end
|
|
}
|
|
|
|
proxy_hide_header Content-Security-Policy;
|
|
proxy_hide_header Content-Security-Policy-Report-Only;
|