# trentpower.fr
# apache configuration
#
# this file is part of the publication layer.
#
# it defines the public contract of the site: what may be served, how long
# it may be trusted, which browser capabilities are denied, and which artefacts
# are allowed to remain durable as part of the archive.
#
# principle:
# active pages are fresh, signed release artefacts are durable, private material
# is never serveable, and security exceptions must be narrow, named, and reviewed.
#
# this file should read like the public constitution of the site:
# strict by default, explicit in its exceptions, fresh for active pages,
# durable for signed archives, and hostile to accidental disclosure.
# ----------------------------------------------------------
# maintenance mode
# ----------------------------------------------------------
# to enable:
# - uncomment the three maintenance lines below
# - comment out the active DirectoryIndex line
#
# to disable:
# - keep the active DirectoryIndex index.html line uncommented
# - keep the maintenance lines commented out
#
# DirectoryIndex maintenance.html index.html
# ErrorDocument 403 /maintenance.html
# ErrorDocument 404 /maintenance.html
DirectoryIndex index.html
# ----------------------------------------------------------
# publication boundary
# ----------------------------------------------------------
# these headers define the browser-side security contract for the site.
#
# the default position is denial: no framing, no ambient permissions,
# no cross-origin trust, and no script execution outside the signed
# publication path.
#
# do not add:
# - analytics exceptions
# - third-party script origins
# - broad connect-src allowances
# - wildcard font, image, or script sources
# - cache immutability for active clean filenames
#
# if a feature appears to need one of these, redesign the feature first.
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "no-referrer"
Header always set X-Permitted-Cross-Domain-Policies "none"
Header always set X-DNS-Prefetch-Control "off"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
# exception: og images
#
# social scrapers need open graph images to be embeddable cross-origin.
# keep the global same-origin resource policy, then make the /images/og/
# exception explicit, narrow, and auditable.
SetEnvIf Request_URI "^/images/og/" IS_OG_IMAGE=1
Header always set Cross-Origin-Resource-Policy "same-origin" env=!IS_OG_IMAGE
Header always set Cross-Origin-Resource-Policy "cross-origin" env=IS_OG_IMAGE
# permissions policy
#
# sensitive browser capabilities are denied by default. the list is trimmed
# to widely recognised directives so chrome does not warn about obsolete or
# experimental feature names.
#
# clipboard-write is allowed for same-origin citation and verification copy
# actions. clipboard-read remains denied.
Header always set Permissions-Policy "accelerometer=(), autoplay=(), camera=(), clipboard-read=(), clipboard-write=(self), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), local-fonts=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), sync-xhr=(), usb=(), web-share=(), window-management=(), xr-spatial-tracking=()"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
# content security policy
#
# default-deny csp. inline script is permitted only by explicit hash.
# do not loosen this policy to make a broken release pass. fix the source,
# regenerate the hash manifest, then redeploy.
#
# json-ld is emitted as application/ld+json and is not treated as executable
# script by modern browsers, so it does not need a script-src hash.
Header always set Content-Security-Policy "default-src 'none'; upgrade-insecure-requests; script-src 'self' 'sha256-lZ+4zCyA6aqvdIO6efMAlaNmrMGCB7twwkiPH87cDcE=' 'sha256-HrAeEk/58uT8a2LzPpipjzb6wWC8IUnsWeDQjZSTNtY=' 'sha256-3dleBgMyQvchfeJrEMqLXKsfGpCGKxEg6sa2/DCjI9w='; script-src-attr 'none'; style-src 'self'; style-src-attr 'none'; font-src 'self'; img-src 'self'; manifest-src 'self'; worker-src 'self'; connect-src 'none'; frame-src 'none'; child-src 'none'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'none'; require-trusted-types-for 'script'; trusted-types tp-i18n"
Header always unset X-Powered-By
# ----------------------------------------------------------
# identity headers
# ----------------------------------------------------------
# html responses advertise the canonical machine-readable person record.
# this keeps identity discovery close to the document surface without making
# non-html assets carry unnecessary metadata.
Header always append Link "; rel=\"alternate\"; type=\"application/ld+json\""
# ----------------------------------------------------------
# language editions
# ----------------------------------------------------------
# language editions should be generated as static html, not translated at
# runtime. each edition should carry its own lang attribute, canonical url,
# hreflang alternates, open graph locale, and structured metadata.
#
# the server should remain language-neutral; the build should own language
# logic.
# ----------------------------------------------------------
# freshness and permanence
# ----------------------------------------------------------
# active pages must stay fresh. frozen release artefacts must remain durable.
# clean active filenames therefore revalidate; archived release assets are
# immutable and tied to their signed edition.
# frozen archive assets
#
# immutable for one year. identified by url prefix rather than filename
# pattern because FilesMatch matches base names only.
#
# this rule is set before the active-asset rules so the environment flag is
# available to gate them.
SetEnvIf Request_URI "^/integrity/releases/[0-9]{4}-[0-9]{2}/assets/" IS_ARCHIVE_ASSET=1
Header always set Cache-Control "public, max-age=31536000, immutable" env=IS_ARCHIVE_ASSET
# all active-asset cache-control rules below are gated with
# env=!IS_ARCHIVE_ASSET so they do not collide with the archive rule when
# FilesMatch also matches a base name inside an archive path.
# html · never cache
#
# html is the live editorial surface. gandi's fronting varnish has previously
# served stale html under weaker directives, so active documents use no-store.
# assets may cache; pages must not.
Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0" env=!IS_ARCHIVE_ASSET
Header set Pragma "no-cache" env=!IS_ARCHIVE_ASSET
Header set Expires "0" env=!IS_ARCHIVE_ASSET
# active css/js/sw · always revalidate
#
# clean filenames mean content may change between editions.
Header set Cache-Control "no-cache, must-revalidate" env=!IS_ARCHIVE_ASSET
Header set Cache-Control "public, max-age=86400, must-revalidate" env=!IS_ARCHIVE_ASSET
# executable asset types
#
# nosniff is enforced globally, so javascript and css must be served with
# explicit mime types. without this, browsers correctly refuse to execute
# same-origin scripts that arrive as text/plain or application/octet-stream.
#
# this protects the policy while avoiding host-level mime-map drift.
Header set Content-Type "application/javascript; charset=utf-8" env=!IS_ARCHIVE_ASSET
Header set Content-Type "text/css; charset=utf-8" env=!IS_ARCHIVE_ASSET
# active fonts
#
# clean filenames revalidate so a font replacement lands. frozen archive
# fonts stay immutable via the IS_ARCHIVE_ASSET gate.
Header set Cache-Control "public, max-age=86400, must-revalidate" env=!IS_ARCHIVE_ASSET
Header set Content-Type "font/woff2"
# active icons · short cache, revalidate
Header set Cache-Control "public, max-age=86400, must-revalidate" env=!IS_ARCHIVE_ASSET
# active jpgs · short cache, revalidate
#
# active filenames are stable but their bytes can change between editions,
# so they must not be immutable.
Header set Cache-Control "public, max-age=86400, must-revalidate" env=!IS_ARCHIVE_ASSET
# identity and metadata files
#
# these files form the machine-readable surface of the publication.
# short caching keeps crawlers efficient while allowing identity and release
# metadata to update intentionally.
Header set Cache-Control "public, max-age=3600"
Header set Content-Type "application/xml; charset=utf-8"
Header set Cache-Control "public, max-age=3600"
Header set Content-Type "text/plain; charset=utf-8"
Header set Cache-Control "public, max-age=86400"
Header set Content-Type "text/plain; charset=utf-8"
Header set Cache-Control "public, max-age=3600"
Header set Content-Type "text/plain; charset=utf-8"
Header set Cache-Control "public, max-age=3600"
Header set Content-Type "application/pgp-keys"
Header set Cache-Control "public, max-age=3600"
Header set Content-Type "application/pgp-signature"
Header set Cache-Control "public, max-age=3600"
Header set Content-Type "application/json; charset=utf-8"
Header set Cache-Control "public, max-age=3600"
Header set Content-Type "application/manifest+json; charset=utf-8"
Header set Cache-Control "no-cache, must-revalidate"
Header set Content-Type "text/plain; charset=utf-8"
Header set Cache-Control "no-cache, must-revalidate"
Header set Content-Type "application/ld+json; charset=utf-8"
Header set Cache-Control "no-cache, must-revalidate"
Header set Content-Type "application/json; charset=utf-8"
Header set Cache-Control "public, max-age=300, must-revalidate"
Header set Content-Type "application/json; charset=utf-8"
Header set Cache-Control "public, max-age=300, must-revalidate"
Header set Content-Type "application/pgp-signature"
# svg assets
#
# avtive svgs revalidate so localised diagrams and editorial graphics update
# on edition bumps. frozen archive svgs inherit the immutable archive rule.
Header set Content-Type "image/svg+xml; charset=utf-8"
Header set Cache-Control "public, max-age=86400, must-revalidate" env=!IS_ARCHIVE_ASSET
# source transparency surface
#
# /source/*.txt files are plain-text mirrors of public files.
# /source/source-manifest.json is the machine index.
#
# they revalidate so mirrors track the active site without a hard cache pin.
SetEnvIf Request_URI "^/source/" IS_SOURCE_MIRROR=1
Header set Content-Type "text/plain; charset=utf-8" env=IS_SOURCE_MIRROR
Header set Cache-Control "no-cache, must-revalidate" env=IS_SOURCE_MIRROR
Header set Content-Type "application/json; charset=utf-8"
Header set Cache-Control "no-cache, must-revalidate"
# ----------------------------------------------------------
# exception: source reader
# ----------------------------------------------------------
# /source/view/ relaxes connect-src to self so the reader can fetch
# /source/*.txt files on demand.
#
# this is intentional: the reader is a transparency surface and the mirrored
# text files are already public. trusted-types adds tp-source-view for innerhtml
# of the tokenised code block. all other directives remain strict.
#
# note: LocationMatch is server-config-only and not valid in .htaccess.
# SetEnvIf plus conditional header directives provide the same route-specific
# effect here.
SetEnvIf Request_URI "^/source/view/" IS_SOURCE_VIEW=1
Header always unset Content-Security-Policy env=IS_SOURCE_VIEW
Header always set Content-Security-Policy "default-src 'none'; upgrade-insecure-requests; script-src 'self' 'sha256-ong18574DRSzuyO+zjuDNWecbI/I+ojY9Bvoi6zBtvw=' 'sha256-HrAeEk/58uT8a2LzPpipjzb6wWC8IUnsWeDQjZSTNtY=' 'sha256-3dleBgMyQvchfeJrEMqLXKsfGpCGKxEg6sa2/DCjI9w='; script-src-attr 'none'; style-src 'self'; style-src-attr 'none'; font-src 'self'; img-src 'self'; manifest-src 'self'; worker-src 'self'; connect-src 'self'; frame-src 'none'; child-src 'none'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'none'; require-trusted-types-for 'script'; trusted-types tp-i18n tp-source-view" env=IS_SOURCE_VIEW
# ----------------------------------------------------------
# exception: service worker and versioned enhancement assets
# ----------------------------------------------------------
# service worker
#
# never immutable, must revalidate. csp is narrowed specifically for sw.js
# so cache.addAll() can fetch same-origin assets. pages still receive the
# strict global csp with connect-src none.
#
# explicit Content-Type and Service-Worker-Allowed are required because
# nosniff is set globally. without an explicit JavaScript mime type, browsers
# may refuse to parse sw.js as a worker script.
Header always set Content-Type "application/javascript; charset=utf-8"
Header always set Service-Worker-Allowed "/"
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header always unset Content-Security-Policy
Header always set Content-Security-Policy "default-src 'none'; script-src 'self'; connect-src 'self'; form-action 'none'; base-uri 'none'"
# source reader scripts
#
# short-cache and revalidate on each edition. these files are unversioned
# because the reader keys them by query parameter.
Header set Content-Type "application/javascript; charset=utf-8"
Header set Cache-Control "no-cache, must-revalidate"
# verification data
#
# versioned files are immutable for one year. the unversioned alias revalidates
# briefly so legacy bookmarks and offline fallbacks stay fresh.
Header set Content-Type "application/javascript; charset=utf-8"
Header set Cache-Control "public, max-age=31536000, immutable"
Header set Content-Type "application/javascript; charset=utf-8"
Header set Cache-Control "public, max-age=300, must-revalidate"
# deferred enhancement bundle and full-font stylesheet
#
# same versioned-alias pattern as verification data. /app.js dynamically loads
# the versioned /app-enhance...js after first paint; that file
# then loads /fonts-full...css. unversioned aliases stay on disk
# for offline and fallback use.
Header set Content-Type "application/javascript; charset=utf-8"
Header set Cache-Control "public, max-age=31536000, immutable"
Header set Content-Type "application/javascript; charset=utf-8"
Header set Cache-Control "no-cache, must-revalidate"
Header set Content-Type "text/css; charset=utf-8"
Header set Cache-Control "public, max-age=31536000, immutable"
Header set Content-Type "text/css; charset=utf-8"
Header set Cache-Control "no-cache, must-revalidate"
# ----------------------------------------------------------
# error documents
# ----------------------------------------------------------
ErrorDocument 403 /403.html
ErrorDocument 404 /404.html
ErrorDocument 500 /500.html
# ----------------------------------------------------------
# directory and server disclosure controls
# ----------------------------------------------------------
Options -Indexes
# suppress the apache version banner from generated error pages.
# ServerTokens prod would also help but is server-config-only and would 500 the
# site if placed here, so it is intentionally omitted.
ServerSignature Off
# ----------------------------------------------------------
# allowed methods
# ----------------------------------------------------------
# static site: only get and head are legitimate. deny everything else at the
# apache level. apache may answer with 403 or 405 depending on build; either is
# acceptable. the invariant is: non-GET/HEAD methods must not return 200.
Require all denied
# ----------------------------------------------------------
# validators
# ----------------------------------------------------------
# Last-Modified reflects filesystem mtime by default. etag derived from mtime
# and size keeps cache validation consistent without exposing inode details.
FileETag MTime Size
# ----------------------------------------------------------
# compression
# ----------------------------------------------------------
# brotli is preferred when available; gzip remains the fallback.
AddOutputFilterByType BROTLI_COMPRESS text/html text/css text/javascript application/javascript application/json application/ld+json application/jrd+json application/manifest+json image/svg+xml text/xml application/xml text/plain
AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json application/ld+json application/jrd+json application/manifest+json image/svg+xml text/xml application/xml text/plain
# ----------------------------------------------------------
# private artefact denylist
# ----------------------------------------------------------
# defence-in-depth rules for files that should never become public, even if a
# local build, manual upload, or deployment script makes a mistake.
# dotfiles
#
# FilesMatch matches filenames, not directories. .well-known files are served by
# explicit public rules and do not start with a dot as filenames, so no exception
# is needed here.
Require all denied
# operational and source file extensions
#
# archive extensions such as zip, tar, gz, and 7z are not denied here because
# signed snapshots are deliberately published under /integrity/releases//.
# stray archives elsewhere are caught by the explicit upload allowlist in the
# build pipeline, not by these header rules.
Require all denied
# explicit hidden config and package-file denial
#
# some of these are already caught by extension rules, but name-based rules are
# easier to audit and survive a future extension allowlist refactor. composer
# and package files are included even though this is a static site, protecting
# against accidental upload of a php or node project.
Require all denied
Require all denied
Require all denied
# private-name patterns
#
# filenames suggesting private or transactional content are denied. public
# artefacts such as pgp-key.asc and integrity.json.sig do not match these
# patterns.
Require all denied
# ----------------------------------------------------------
# accidental-upload quarantine
# ----------------------------------------------------------
# sensitive directories are forbidden if accidentally uploaded. RedirectMatch 403
# returns forbidden without creating a redirect loop and is safe with varnish.
RedirectMatch 403 ^/docs/
RedirectMatch 403 ^/var/
RedirectMatch 403 ^/_archives/
RedirectMatch 403 ^/_licences/
RedirectMatch 403 ^/_private/
RedirectMatch 403 ^/private/
RedirectMatch 403 ^/node_modules/
RedirectMatch 403 ^/src/
RedirectMatch 403 ^/tests?/
RedirectMatch 403 ^/backups?/
RedirectMatch 403 ^/htdocs/htdocs/
# hidden vcs, editor, build, and template paths
RedirectMatch 403 ^/\.git(/|$)
RedirectMatch 403 ^/\.github(/|$)
RedirectMatch 403 ^/\.vscode(/|$)
RedirectMatch 403 ^/\.idea(/|$)
RedirectMatch 403 ^/tools(/|$)
RedirectMatch 403 ^/templates(/|$)