# 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(/|$)