diff --git a/CHANGELOG.md b/CHANGELOG.md index 370d22c4a..25643d32e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ service can change a gate's answer, and a pull request number is only knowable f ## 2026-10-19 +- Twenty more guides move to the two-section layout (maintainer request, 2026-10-18), with each guide's own tool named first in its header; the header now says "1 more recorded basis" rather than "bases", and the headers of guides already in the layout are regenerated where that changes them; no guide body or digest changes. Guides: `firebase-supabase.md`, `pocketbase.md`, `ray.md`, `gitops-controllers.md`, `nomad-consul.md`, `self-hosted-ci-runners.md`, `workflow-orchestrators.md`, `kubernetes.md`, `nginx.md`, `apache.md`, `caddy.md`, `traefik.md`, `haproxy.md`, `lighttpd.md`, `free-certificates.md`, `self-signed.md`, `headers.md`, `cors.md`, `docker.md`, `container-hardening.md` (#693) - `authentication.md`: the fail2ban rule now cites fail2ban 1.1.1's shipped `sshd`, `nginx-http-auth` and `webmin-auth` filters and jails, which are disabled by default; enabling the relevant jails with matching log paths is labelled as this guide's advice, and coverage of any particular login panel and how easy it is to set up are stated as not established; TODO row 1.293 moves to DONE (#694) - `authentication.md`: the Cloudflare Access rule now cites Cloudflare's One-time PIN, identity-provider and generic OIDC pages for the login options, and its existing self-hosted web application scope now reads "published through Access", stating that use with any application unchanged is not established; no command changes; TODO row 1.292 moves to DONE (#692) - Twenty more guides move to the two-section layout (maintainer request, 2026-10-18), with each guide's own tool named first in its header and components listed headline tools first; no guide body or digest changes. Guides: `llm-observability.md`, `agent-builders.md`, `ai-infra-services.md`, `vector-databases.md`, `search-engines.md`, `elasticsearch.md`, `minio.md`, `object-storage.md`, `jupyter.md`, `gradio.md`, `streamlit.md`, `code-server.md`, `n8n.md`, `headless-browser-services.md`, `headless-cms-instant-api.md`, `supabase-self-hosted.md`, `observability-components.md`, `self-hosted-error-trackers.md`, `realtime-webhooks.md`, `realtime-voice-infra.md` (#691) diff --git a/apache.md b/apache.md index 118738be8..6659c4fdd 100644 --- a/apache.md +++ b/apache.md @@ -1,201 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-18", - "documentation_checked": "2026-09", - "body_sha256": "645f3f5ab27a4df2af69e0680ac8e98f85ca416fe85a078c95075598e9b33f4d", - "components": { - "apache": { - "name": "Apache HTTP Server", - "basis": "2.4", - "sources": { - "sde155f1667f3": "https://httpd.apache.org/docs/2.4/ssl/ssl_howto.html", - "s99dcb49d86a4": "https://httpd.apache.org/docs/2.4/howto/auth.html", - "sc07f97af99e3": "https://httpd.apache.org/docs/2.4/vhosts/name-based.html", - "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy", - "s7c302c734a0d": "https://httpd.apache.org/docs/2.4/vhosts/details.html", - "s1dbb2636d050": "https://httpd.apache.org/docs/2.4/mod/mod_headers.html" - } - }, - "policy": { - "name": "Mozilla TLS policy", - "basis": "unknown", - "sources": { - "s529b0eabe2ed": "https://ssl-config.mozilla.org/" - } - }, - "tls-min": { - "name": "Apache TLSv1.3 minimum", - "basis": "2.4.36+", - "sources": { - "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" - } - }, - "sni-min": { - "name": "Apache per-vhost protocol minimum", - "basis": "2.4.42+", - "sources": { - "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" - } - }, - "openssl": { - "name": "OpenSSL qualification", - "basis": "1.1.1+", - "sources": { - "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" - } - }, - "chain-min": { - "name": "Apache chain-file minimum", - "basis": "2.4.8", - "sources": { - "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" - } - }, - "certbot": { - "name": "Certbot Apache installation", - "basis": "v2.3.0", - "sources": { - "sc74fcd0167d8": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L104-L106", - "sa453a66e4130": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/client.py#L646", - "sacf90ffa5a6d": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/client.py#L674-L677", - "s74412ad87016": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/constants.py#L66-L68" - } - }, - "debian-packaging": { - "name": "Debian apache2 packaging", - "basis": "2.4.69-1", - "sources": { - "s1c05297f1ef0": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.install#L1-7", - "s32bd234fb40a": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/config-dir/sites-available/default-ssl.conf#L1-4", - "sc9b152b1a444": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.links#L7", - "sd1c9de6b39aa": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/control#L92-102", - "s62f6ed2866ab": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1" - } - }, - "fedora-packaging": { - "name": "Fedora f44 httpd packaging", - "basis": "commit 6df70d39", - "sources": { - "s7d73f140a3fe": "https://src.fedoraproject.org/rpms/httpd/blob/6df70d392dace11a7c01ceffc5d8a929de0b7a35/f/httpd.spec#_204-212", - "s4479577a2e6a": "https://src.fedoraproject.org/rpms/httpd/blob/6df70d392dace11a7c01ceffc5d8a929de0b7a35/f/httpd.spec#_819-824" - } - }, - "htpasswd": { - "name": "httpd htpasswd", - "basis": "2.4.69", - "sources": { - "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128", - "s7850e1e9dfa1": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L43-L45", - "s1c1a101a70e7": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L290-L295" - } - }, - "owasp": { - "name": "OWASP password storage cheat sheet", - "basis": "commit 72865b1e", - "sources": { - "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" - } - }, - "authelia": { - "name": "Authelia proxy support", - "basis": "v4.39.28", - "sources": { - "s1de7fe72d011": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L112-L115" - } - }, - "cloudflare-access": { - "name": "Cloudflare Access documentation", - "basis": "commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104", - "sources": { - "sfddb92e438ca": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L14-L16", - "s7cafe1b17afd": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54", - "s603bdb6b24ab": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L45-L45", - "s863803bd9a1f": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L48-L48", - "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" - } - }, - "openidc": { - "name": "mod_auth_openidc", - "basis": "v2.4.20.3", - "sources": { - "s01e183d16f03": "https://github.com/OpenIDC/mod_auth_openidc/blob/v2.4.20.3/README.md#L11-L20" - } - } - }, - "claims": { - "certbot": {"text": "The guide says certbot --apache installs certificates and redirects by default; --redirect is explicit and --hsts defaults off.", "components": ["apache", "certbot"], "sources": ["apache:sde155f1667f3", "certbot:sc74fcd0167d8", "certbot:sa453a66e4130", "certbot:sacf90ffa5a6d", "certbot:s74412ad87016"], "status": "REASONED"}, - "modules": {"text": "Enable ssl/headers and a 443 vhost on Debian/Ubuntu, or install mod_ssl/httpd-tools on RHEL/Fedora; pinned packaging shows a2enmod, a2ensite (a link to a2enmod), default-ssl, and Fedora package names.", "components": ["apache", "debian-packaging", "fedora-packaging"], "sources": ["apache:sde155f1667f3", "debian-packaging:s62f6ed2866ab", "debian-packaging:s1c05297f1ef0", "debian-packaging:sc9b152b1a444", "debian-packaging:s32bd234fb40a", "fedora-packaging:s7d73f140a3fe"], "status": "REASONED"}, - "tls": {"text": "The 443 vhost enables TLS with a certificate chain and private key and TLSv1.2/TLSv1.3 only.", "components": ["apache"], "sources": ["apache:sde155f1667f3", "apache:scf804f4940d7"], "status": "REASONED"}, - "tls-version": {"text": "TLSv1.3 needs Apache 2.4.36+ with OpenSSL 1.1.1+; older builds use SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1.", "components": ["apache", "tls-min", "openssl"], "sources": ["apache:scf804f4940d7", "tls-min:scf804f4940d7", "openssl:scf804f4940d7"], "status": "REASONED"}, - "vhost-floor": {"text": "Independent per-vhost SSLProtocol needs Apache 2.4.42+, OpenSSL 1.1.1+ and client SNI; older builds also need the floor in global/base config.", "components": ["apache", "sni-min", "openssl"], "sources": ["apache:scf804f4940d7", "apache:s7c302c734a0d", "sni-min:scf804f4940d7", "openssl:scf804f4940d7"], "status": "REASONED"}, - "chain": {"text": "Apache 2.4.8+ permits the chain in SSLCertificateFile; SSLCertificateChainFile is deprecated.", "components": ["apache", "chain-min"], "sources": ["apache:scf804f4940d7", "chain-min:scf804f4940d7"], "status": "REASONED"}, - "hsts": {"text": "Header always sets HSTS after HTTPS works; includeSubDomains requires valid HTTPS everywhere.", "components": ["apache"], "sources": ["apache:sde155f1667f3", "apache:s1dbb2636d050"], "status": "REASONED"}, - "cipher-policy": {"text": "Generate explicit cipher policy with Mozilla rather than copying old lists.", "components": ["policy"], "sources": ["policy:s529b0eabe2ed"], "status": "REASONED"}, - "redirect": {"text": "The 80 vhost sends a permanent redirect to the canonical HTTPS host; retain only redirects and needed ACME HTTP-01 paths.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:sde155f1667f3"], "status": "REASONED"}, - "password-file": {"text": "Create a bcrypt htpasswd file with cost 12 and -c only for its first user; httpd 2.4.69 defaults bcrypt cost to 5 and OWASP recommends at least 10 for legacy bcrypt. Both password-file paths are guide choices.", "components": ["apache", "debian-packaging", "fedora-packaging", "htpasswd", "owasp"], "sources": ["apache:s99dcb49d86a4", "debian-packaging:s62f6ed2866ab", "debian-packaging:sd1c9de6b39aa", "fedora-packaging:s4479577a2e6a", "htpasswd:s309ecdd34379", "htpasswd:s7850e1e9dfa1", "htpasswd:s1c1a101a70e7", "owasp:s4531e65b6502"], "status": "REASONED"}, - "basic": {"text": "AuthType Basic, AuthName, AuthUserFile and Require valid-user protect the chosen site/path over TLS; enumerate every public path.", "components": ["apache"], "sources": ["apache:s99dcb49d86a4"], "status": "REASONED"}, - "mtls": {"text": "A separate machine-client 443 vhost requires a trusted certificate with SSLVerifyClient require and depth 2; SSLCACertificateFile is server/vhost scoped.", "components": ["apache"], "sources": ["apache:scf804f4940d7"], "status": "REASONED"}, - "mfa": {"text": "Basic is single-factor; Authelia v4.39.28 says Apache is unsupported for its proxy integration, and mod_auth_openidc v2.4.20.3 is an OIDC relying party. Provider MFA is guide advice; Cloudflare Access documents self-hosted application and MFA policy controls; fronting an Apache origin is this guide's suggested option and assessment. This guide assesses that an Apache origin reachable without passing through Access is not protected by it.", "components": ["apache", "authelia", "openidc", "cloudflare-access"], "sources": ["apache:s99dcb49d86a4", "authelia:s1de7fe72d011", "openidc:s01e183d16f03", "cloudflare-access:sfddb92e438ca", "cloudflare-access:s7cafe1b17afd", "cloudflare-access:sdcac732541da", "cloudflare-access:s603bdb6b24ab", "cloudflare-access:s863803bd9a1f"], "status": "REASONED"}, - "key-permissions": {"text": "Keep private keys root-owned and mode 0600.", "components": ["apache"], "sources": ["apache:sde155f1667f3"], "status": "REASONED"}, - "verify-config": {"text": "Run apachectl configtest before service reload; no outcome is recorded.", "components": ["apache"], "sources": ["apache:sde155f1667f3"], "status": "REASONED", "verify": [1]}, - "verify-redirect": {"text": "HTTP should return 301 with an HTTPS Location.", "components": ["apache"], "sources": ["apache:sc07f97af99e3"], "status": "REASONED", "verify": [1]}, - "verify-auth": {"text": "Read printed status: curl can exit 0 for 200 or 401; expect 200 before auth and 401 after it.", "components": ["apache"], "sources": ["apache:s99dcb49d86a4"], "status": "REASONED", "verify": [1]}, - "verify-listeners": {"text": "Inspect the entire listener table, without filtering away unexpected ports.", "components": ["apache"], "sources": ["apache:sc07f97af99e3"], "status": "REASONED", "verify": [1]}, - "verify-unmatched": {"text": "Use an unconfigured certificate-covered URL name for both SNI and Host; 401 passes, 200 exposes a protected resource through another vhost. Host-only probes can misleadingly give 401 or 421.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:scf804f4940d7", "apache:s7c302c734a0d"], "status": "REASONED", "verify": [1]}, - "verify-vhost-review": {"text": "If no spare certificate-covered name exists, use apachectl -S and review that the first matching vhost cannot serve the protected DocumentRoot.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:s7c302c734a0d"], "status": "REASONED", "verify": [2]} - } -} ---- # Apache HTTP Server: TLS and authentication - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-18; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- apache: Apache HTTP Server (2.4) -- policy: Mozilla TLS policy (unknown) -- tls-min: Apache TLSv1.3 minimum (2.4.36+) -- sni-min: Apache per-vhost protocol minimum (2.4.42+) -- openssl: OpenSSL qualification (1.1.1+) -- chain-min: Apache chain-file minimum (2.4.8) -- certbot: Certbot Apache installation (v2.3.0) -- debian-packaging: Debian apache2 packaging (2.4.69-1) -- fedora-packaging: Fedora f44 httpd packaging (commit 6df70d39) -- htpasswd: httpd htpasswd (2.4.69) -- owasp: OWASP password storage cheat sheet (commit 72865b1e) -- authelia: Authelia proxy support (v4.39.28) -- cloudflare-access: Cloudflare Access documentation (commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- openidc: mod_auth_openidc (v2.4.20.3) - -Claims (full source mapping in the front matter): +Written against Apache HTTP Server 2.4, Apache TLSv1.3 minimum 2.4.36+, Apache per-vhost protocol minimum 2.4.42+, Apache chain-file minimum 2.4.8, httpd htpasswd 2.4.69, OpenSSL qualification 1.1.1+, Certbot Apache installation v2.3.0, and Debian apache2 packaging 2.4.69-1, plus 5 more recorded bases; 1 further source carries no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 20 claims, all reasoned from cited sources. -- certbot (REASONED; apache, certbot): The guide says certbot --apache installs certificates and redirects by default; --redirect is explicit and --hsts defaults off. -- modules (REASONED; apache, debian-packaging, fedora-packaging): Enable ssl/headers and a 443 vhost on Debian/Ubuntu, or install mod_ssl/httpd-tools on RHEL/Fedora; pinned packaging shows a2enmod, a2ensite (a link to a2enmod), default-ssl, and Fedora package names. -- tls (REASONED; apache): The 443 vhost enables TLS with a certificate chain and private key and TLSv1.2/TLSv1.3 only. -- tls-version (REASONED; apache, tls-min, openssl): TLSv1.3 needs Apache 2.4.36+ with OpenSSL 1.1.1+; older builds use SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1. -- vhost-floor (REASONED; apache, sni-min, openssl): Independent per-vhost SSLProtocol needs Apache 2.4.42+, OpenSSL 1.1.1+ and client SNI; older builds also need the floor in global/base config. -- chain (REASONED; apache, chain-min): Apache 2.4.8+ permits the chain in SSLCertificateFile; SSLCertificateChainFile is deprecated. -- hsts (REASONED; apache): Header always sets HSTS after HTTPS works; includeSubDomains requires valid HTTPS everywhere. -- cipher-policy (REASONED; policy): Generate explicit cipher policy with Mozilla rather than copying old lists. -- redirect (REASONED; apache): The 80 vhost sends a permanent redirect to the canonical HTTPS host; retain only redirects and needed ACME HTTP-01 paths. -- password-file (REASONED; apache, debian-packaging, fedora-packaging, htpasswd, owasp): Create a bcrypt htpasswd file with cost 12 and -c only for its first user; httpd 2.4.69 defaults bcrypt cost to 5 and OWASP recommends at least 10 for legacy bcrypt. Both password-file paths are guide choices. -- basic (REASONED; apache): AuthType Basic, AuthName, AuthUserFile and Require valid-user protect the chosen site/path over TLS; enumerate every public path. -- mtls (REASONED; apache): A separate machine-client 443 vhost requires a trusted certificate with SSLVerifyClient require and depth 2; SSLCACertificateFile is server/vhost scoped. -- mfa (REASONED; apache, authelia, openidc, cloudflare-access): Basic is single-factor; Authelia v4.39.28 says Apache is unsupported for its proxy integration, and mod_auth_openidc v2.4.20.3 is an OIDC relying party. Provider MFA is guide advice; Cloudflare Access documents self-hosted application and MFA policy controls; fronting an Apache origin is this guide's suggested option and assessment. This guide assesses that an Apache origin reachable without passing through Access is not protected by it. -- key-permissions (REASONED; apache): Keep private keys root-owned and mode 0600. -- verify-config (REASONED; apache): Run apachectl configtest before service reload; no outcome is recorded. -- verify-redirect (REASONED; apache): HTTP should return 301 with an HTTPS Location. -- verify-auth (REASONED; apache): Read printed status: curl can exit 0 for 200 or 401; expect 200 before auth and 401 after it. -- verify-listeners (REASONED; apache): Inspect the entire listener table, without filtering away unexpected ports. -- verify-unmatched (REASONED; apache): Use an unconfigured certificate-covered URL name for both SNI and Host; 401 passes, 200 exposes a protected resource through another vhost. Host-only probes can misleadingly give 401 or 421. -- verify-vhost-review (REASONED; apache): If no spare certificate-covered name exists, use apachectl -S and review that the first matching vhost cannot serve the protected DocumentRoot. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Applies to Apache 2.4. Get a certificate first: [free-certificates.md](free-certificates.md) for a public host (note that `certbot --apache` obtains and installs the certificate and, at the time of writing, enables the HTTP-to-HTTPS redirect by default (`--redirect` requests it explicitly), but review its generated TLS config against steps 1 to 3 and add HSTS yourself, since certbot's `--hsts` is off by default), or [self-signed.md](self-signed.md) for internal use. @@ -353,3 +160,222 @@ sudo apachectl -S # the vhost list, in Apache's own matchi - Cloudflare Access origin reachability at commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L45-L45 ; https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L48-L48 - Cloudflare Access MFA policy controls at commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81 - mod_auth_openidc v2.4.20.3 relying-party description: https://github.com/OpenIDC/mod_auth_openidc/blob/v2.4.20.3/README.md#L11-L20 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/apache.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- apache: Apache HTTP Server (2.4) +- tls-min: Apache TLSv1.3 minimum (2.4.36+) +- sni-min: Apache per-vhost protocol minimum (2.4.42+) +- chain-min: Apache chain-file minimum (2.4.8) +- htpasswd: httpd htpasswd (2.4.69) +- openssl: OpenSSL qualification (1.1.1+) +- certbot: Certbot Apache installation (v2.3.0) +- debian-packaging: Debian apache2 packaging (2.4.69-1) +- fedora-packaging: Fedora f44 httpd packaging (commit 6df70d39) +- authelia: Authelia proxy support (v4.39.28) +- cloudflare-access: Cloudflare Access documentation (commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- openidc: mod_auth_openidc (v2.4.20.3) +- policy: Mozilla TLS policy (unknown) +- owasp: OWASP password storage cheat sheet (commit 72865b1e) + +Claims (full source mapping in the JSON block below): + +- certbot (REASONED; apache, certbot): The guide says certbot --apache installs certificates and redirects by default; --redirect is explicit and --hsts defaults off. +- modules (REASONED; apache, debian-packaging, fedora-packaging): Enable ssl/headers and a 443 vhost on Debian/Ubuntu, or install mod_ssl/httpd-tools on RHEL/Fedora; pinned packaging shows a2enmod, a2ensite (a link to a2enmod), default-ssl, and Fedora package names. +- tls (REASONED; apache): The 443 vhost enables TLS with a certificate chain and private key and TLSv1.2/TLSv1.3 only. +- tls-version (REASONED; apache, tls-min, openssl): TLSv1.3 needs Apache 2.4.36+ with OpenSSL 1.1.1+; older builds use SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1. +- vhost-floor (REASONED; apache, sni-min, openssl): Independent per-vhost SSLProtocol needs Apache 2.4.42+, OpenSSL 1.1.1+ and client SNI; older builds also need the floor in global/base config. +- chain (REASONED; apache, chain-min): Apache 2.4.8+ permits the chain in SSLCertificateFile; SSLCertificateChainFile is deprecated. +- hsts (REASONED; apache): Header always sets HSTS after HTTPS works; includeSubDomains requires valid HTTPS everywhere. +- cipher-policy (REASONED; policy): Generate explicit cipher policy with Mozilla rather than copying old lists. +- redirect (REASONED; apache): The 80 vhost sends a permanent redirect to the canonical HTTPS host; retain only redirects and needed ACME HTTP-01 paths. +- password-file (REASONED; apache, debian-packaging, fedora-packaging, htpasswd, owasp): Create a bcrypt htpasswd file with cost 12 and -c only for its first user; httpd 2.4.69 defaults bcrypt cost to 5 and OWASP recommends at least 10 for legacy bcrypt. Both password-file paths are guide choices. +- basic (REASONED; apache): AuthType Basic, AuthName, AuthUserFile and Require valid-user protect the chosen site/path over TLS; enumerate every public path. +- mtls (REASONED; apache): A separate machine-client 443 vhost requires a trusted certificate with SSLVerifyClient require and depth 2; SSLCACertificateFile is server/vhost scoped. +- mfa (REASONED; apache, authelia, openidc, cloudflare-access): Basic is single-factor; Authelia v4.39.28 says Apache is unsupported for its proxy integration, and mod_auth_openidc v2.4.20.3 is an OIDC relying party. Provider MFA is guide advice; Cloudflare Access documents self-hosted application and MFA policy controls; fronting an Apache origin is this guide's suggested option and assessment. This guide assesses that an Apache origin reachable without passing through Access is not protected by it. +- key-permissions (REASONED; apache): Keep private keys root-owned and mode 0600. +- verify-config (REASONED; apache): Run apachectl configtest before service reload; no outcome is recorded. +- verify-redirect (REASONED; apache): HTTP should return 301 with an HTTPS Location. +- verify-auth (REASONED; apache): Read printed status: curl can exit 0 for 200 or 401; expect 200 before auth and 401 after it. +- verify-listeners (REASONED; apache): Inspect the entire listener table, without filtering away unexpected ports. +- verify-unmatched (REASONED; apache): Use an unconfigured certificate-covered URL name for both SNI and Host; 401 passes, 200 exposes a protected resource through another vhost. Host-only probes can misleadingly give 401 or 421. +- verify-vhost-review (REASONED; apache): If no spare certificate-covered name exists, use apachectl -S and review that the first matching vhost cannot serve the protected DocumentRoot. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "645f3f5ab27a4df2af69e0680ac8e98f85ca416fe85a078c95075598e9b33f4d", + "components": { + "apache": { + "name": "Apache HTTP Server", + "basis": "2.4", + "sources": { + "sde155f1667f3": "https://httpd.apache.org/docs/2.4/ssl/ssl_howto.html", + "s99dcb49d86a4": "https://httpd.apache.org/docs/2.4/howto/auth.html", + "sc07f97af99e3": "https://httpd.apache.org/docs/2.4/vhosts/name-based.html", + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy", + "s7c302c734a0d": "https://httpd.apache.org/docs/2.4/vhosts/details.html", + "s1dbb2636d050": "https://httpd.apache.org/docs/2.4/mod/mod_headers.html" + } + }, + "tls-min": { + "name": "Apache TLSv1.3 minimum", + "basis": "2.4.36+", + "sources": { + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" + } + }, + "sni-min": { + "name": "Apache per-vhost protocol minimum", + "basis": "2.4.42+", + "sources": { + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" + } + }, + "chain-min": { + "name": "Apache chain-file minimum", + "basis": "2.4.8", + "sources": { + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" + } + }, + "htpasswd": { + "name": "httpd htpasswd", + "basis": "2.4.69", + "sources": { + "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128", + "s7850e1e9dfa1": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L43-L45", + "s1c1a101a70e7": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L290-L295" + } + }, + "openssl": { + "name": "OpenSSL qualification", + "basis": "1.1.1+", + "sources": { + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" + } + }, + "certbot": { + "name": "Certbot Apache installation", + "basis": "v2.3.0", + "sources": { + "sc74fcd0167d8": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L104-L106", + "sa453a66e4130": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/client.py#L646", + "sacf90ffa5a6d": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/client.py#L674-L677", + "s74412ad87016": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/constants.py#L66-L68" + } + }, + "debian-packaging": { + "name": "Debian apache2 packaging", + "basis": "2.4.69-1", + "sources": { + "s1c05297f1ef0": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.install#L1-7", + "s32bd234fb40a": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/config-dir/sites-available/default-ssl.conf#L1-4", + "sc9b152b1a444": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.links#L7", + "sd1c9de6b39aa": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/control#L92-102", + "s62f6ed2866ab": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1" + } + }, + "fedora-packaging": { + "name": "Fedora f44 httpd packaging", + "basis": "commit 6df70d39", + "sources": { + "s7d73f140a3fe": "https://src.fedoraproject.org/rpms/httpd/blob/6df70d392dace11a7c01ceffc5d8a929de0b7a35/f/httpd.spec#_204-212", + "s4479577a2e6a": "https://src.fedoraproject.org/rpms/httpd/blob/6df70d392dace11a7c01ceffc5d8a929de0b7a35/f/httpd.spec#_819-824" + } + }, + "authelia": { + "name": "Authelia proxy support", + "basis": "v4.39.28", + "sources": { + "s1de7fe72d011": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L112-L115" + } + }, + "cloudflare-access": { + "name": "Cloudflare Access documentation", + "basis": "commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "sfddb92e438ca": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L14-L16", + "s7cafe1b17afd": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54", + "s603bdb6b24ab": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L45-L45", + "s863803bd9a1f": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L48-L48", + "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" + } + }, + "openidc": { + "name": "mod_auth_openidc", + "basis": "v2.4.20.3", + "sources": { + "s01e183d16f03": "https://github.com/OpenIDC/mod_auth_openidc/blob/v2.4.20.3/README.md#L11-L20" + } + }, + "policy": { + "name": "Mozilla TLS policy", + "basis": "unknown", + "sources": { + "s529b0eabe2ed": "https://ssl-config.mozilla.org/" + } + }, + "owasp": { + "name": "OWASP password storage cheat sheet", + "basis": "commit 72865b1e", + "sources": { + "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" + } + } + }, + "claims": { + "certbot": {"text": "The guide says certbot --apache installs certificates and redirects by default; --redirect is explicit and --hsts defaults off.", "components": ["apache", "certbot"], "sources": ["apache:sde155f1667f3", "certbot:sc74fcd0167d8", "certbot:sa453a66e4130", "certbot:sacf90ffa5a6d", "certbot:s74412ad87016"], "status": "REASONED"}, + "modules": {"text": "Enable ssl/headers and a 443 vhost on Debian/Ubuntu, or install mod_ssl/httpd-tools on RHEL/Fedora; pinned packaging shows a2enmod, a2ensite (a link to a2enmod), default-ssl, and Fedora package names.", "components": ["apache", "debian-packaging", "fedora-packaging"], "sources": ["apache:sde155f1667f3", "debian-packaging:s62f6ed2866ab", "debian-packaging:s1c05297f1ef0", "debian-packaging:sc9b152b1a444", "debian-packaging:s32bd234fb40a", "fedora-packaging:s7d73f140a3fe"], "status": "REASONED"}, + "tls": {"text": "The 443 vhost enables TLS with a certificate chain and private key and TLSv1.2/TLSv1.3 only.", "components": ["apache"], "sources": ["apache:sde155f1667f3", "apache:scf804f4940d7"], "status": "REASONED"}, + "tls-version": {"text": "TLSv1.3 needs Apache 2.4.36+ with OpenSSL 1.1.1+; older builds use SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1.", "components": ["apache", "tls-min", "openssl"], "sources": ["apache:scf804f4940d7", "tls-min:scf804f4940d7", "openssl:scf804f4940d7"], "status": "REASONED"}, + "vhost-floor": {"text": "Independent per-vhost SSLProtocol needs Apache 2.4.42+, OpenSSL 1.1.1+ and client SNI; older builds also need the floor in global/base config.", "components": ["apache", "sni-min", "openssl"], "sources": ["apache:scf804f4940d7", "apache:s7c302c734a0d", "sni-min:scf804f4940d7", "openssl:scf804f4940d7"], "status": "REASONED"}, + "chain": {"text": "Apache 2.4.8+ permits the chain in SSLCertificateFile; SSLCertificateChainFile is deprecated.", "components": ["apache", "chain-min"], "sources": ["apache:scf804f4940d7", "chain-min:scf804f4940d7"], "status": "REASONED"}, + "hsts": {"text": "Header always sets HSTS after HTTPS works; includeSubDomains requires valid HTTPS everywhere.", "components": ["apache"], "sources": ["apache:sde155f1667f3", "apache:s1dbb2636d050"], "status": "REASONED"}, + "cipher-policy": {"text": "Generate explicit cipher policy with Mozilla rather than copying old lists.", "components": ["policy"], "sources": ["policy:s529b0eabe2ed"], "status": "REASONED"}, + "redirect": {"text": "The 80 vhost sends a permanent redirect to the canonical HTTPS host; retain only redirects and needed ACME HTTP-01 paths.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:sde155f1667f3"], "status": "REASONED"}, + "password-file": {"text": "Create a bcrypt htpasswd file with cost 12 and -c only for its first user; httpd 2.4.69 defaults bcrypt cost to 5 and OWASP recommends at least 10 for legacy bcrypt. Both password-file paths are guide choices.", "components": ["apache", "debian-packaging", "fedora-packaging", "htpasswd", "owasp"], "sources": ["apache:s99dcb49d86a4", "debian-packaging:s62f6ed2866ab", "debian-packaging:sd1c9de6b39aa", "fedora-packaging:s4479577a2e6a", "htpasswd:s309ecdd34379", "htpasswd:s7850e1e9dfa1", "htpasswd:s1c1a101a70e7", "owasp:s4531e65b6502"], "status": "REASONED"}, + "basic": {"text": "AuthType Basic, AuthName, AuthUserFile and Require valid-user protect the chosen site/path over TLS; enumerate every public path.", "components": ["apache"], "sources": ["apache:s99dcb49d86a4"], "status": "REASONED"}, + "mtls": {"text": "A separate machine-client 443 vhost requires a trusted certificate with SSLVerifyClient require and depth 2; SSLCACertificateFile is server/vhost scoped.", "components": ["apache"], "sources": ["apache:scf804f4940d7"], "status": "REASONED"}, + "mfa": {"text": "Basic is single-factor; Authelia v4.39.28 says Apache is unsupported for its proxy integration, and mod_auth_openidc v2.4.20.3 is an OIDC relying party. Provider MFA is guide advice; Cloudflare Access documents self-hosted application and MFA policy controls; fronting an Apache origin is this guide's suggested option and assessment. This guide assesses that an Apache origin reachable without passing through Access is not protected by it.", "components": ["apache", "authelia", "openidc", "cloudflare-access"], "sources": ["apache:s99dcb49d86a4", "authelia:s1de7fe72d011", "openidc:s01e183d16f03", "cloudflare-access:sfddb92e438ca", "cloudflare-access:s7cafe1b17afd", "cloudflare-access:sdcac732541da", "cloudflare-access:s603bdb6b24ab", "cloudflare-access:s863803bd9a1f"], "status": "REASONED"}, + "key-permissions": {"text": "Keep private keys root-owned and mode 0600.", "components": ["apache"], "sources": ["apache:sde155f1667f3"], "status": "REASONED"}, + "verify-config": {"text": "Run apachectl configtest before service reload; no outcome is recorded.", "components": ["apache"], "sources": ["apache:sde155f1667f3"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP should return 301 with an HTTPS Location.", "components": ["apache"], "sources": ["apache:sc07f97af99e3"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "Read printed status: curl can exit 0 for 200 or 401; expect 200 before auth and 401 after it.", "components": ["apache"], "sources": ["apache:s99dcb49d86a4"], "status": "REASONED", "verify": [1]}, + "verify-listeners": {"text": "Inspect the entire listener table, without filtering away unexpected ports.", "components": ["apache"], "sources": ["apache:sc07f97af99e3"], "status": "REASONED", "verify": [1]}, + "verify-unmatched": {"text": "Use an unconfigured certificate-covered URL name for both SNI and Host; 401 passes, 200 exposes a protected resource through another vhost. Host-only probes can misleadingly give 401 or 421.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:scf804f4940d7", "apache:s7c302c734a0d"], "status": "REASONED", "verify": [1]}, + "verify-vhost-review": {"text": "If no spare certificate-covered name exists, use apachectl -S and review that the first matching vhost cannot serve the protected DocumentRoot.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:s7c302c734a0d"], "status": "REASONED", "verify": [2]} + } +} +```` + diff --git a/caddy.md b/caddy.md index 35759f8f1..1fa5e8fe7 100644 --- a/caddy.md +++ b/caddy.md @@ -1,206 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-27", - "documentation_checked": "2026-09", - "body_sha256": "7a5caab2cbcf90c693eab11084c5f8aebc6a3bc8e613f7ab76ca8167e3a9ea28", - "components": { - "admin": { - "name": "Caddy admin source", - "basis": "v2.11.4", - "sources": { - "s35044463fa67": "https://github.com/caddyserver/caddy/blob/v2.11.4/admin.go#L1433", - "s2bbcf239df02": "https://github.com/caddyserver/caddy/blob/v2.11.4/admin.go#L58-L67" - } - }, - "tags": { - "name": "Official image tag map", - "basis": "d82ca5102fa6735be29d5e1fc6ce03af77eb091e", - "sources": { - "sdc8aca42f5e2": "https://github.com/docker-library/official-images/blob/d82ca5102fa6735be29d5e1fc6ce03af77eb091e/library/caddy#L7-L65" - } - }, - "images": { - "name": "Caddy Docker source", - "basis": "fba2853501d36e8a72f946ac8cb7ff64d07e48f2", - "sources": { - "s0f0994ef9fac": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L17", - "s2dce0bbf3a70": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L59-L63", - "sd2c1b05a8088": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2022/Dockerfile#L27-L32", - "sf3e9c2d4e0cd": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2022/Dockerfile#L8", - "sc5dd3f9746fe": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2025/Dockerfile#L27-L32", - "s6c800a156eac": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2025/Dockerfile#L8", - "sc5c82704d33f": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2022/Dockerfile#L10", - "sc1cb0e1fb9eb": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2022/Dockerfile#L42-L47", - "s11528dfcc516": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2025/Dockerfile#L10", - "s70b76475a281": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2025/Dockerfile#L42-L47" - } - }, - "dist": { - "name": "Default Caddyfile source", - "basis": "33ae08ff08d168572df2956ed14fbc4949880d94", - "sources": { - "s433758a90af0": "https://github.com/caddyserver/dist/blob/33ae08ff08d168572df2956ed14fbc4949880d94/config/Caddyfile" - } - }, - "docker-network": { - "name": "Docker networking documentation", - "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", - "sources": { - "se1437c0f19fb": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L286", - "sfce645f8b46e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/drivers/host.md#L11-L13", - "sfd29f33e4108": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L302-L325" - } - }, - "docs-rolling": { - "name": "Caddy documentation (rolling)", - "basis": "unknown", - "sources": { - "s434e7b7d7f63": "https://caddyserver.com/docs/automatic-https", - "s3923477355fb": "https://caddyserver.com/docs/api", - "sdbcd496895d6": "https://caddyserver.com/docs/caddyfile/options", - "s31af5ed45a20": "https://caddyserver.com/docs/caddyfile/directives/request_body", - "sec59a61934c5": "https://caddyserver.com/docs/caddyfile/directives", - "sbc2bd9599bd1": "https://caddyserver.com/docs/caddyfile/directives/basic_auth", - "s2e47c3139bd2": "https://caddyserver.com/docs/caddyfile/directives/tls", - "s3865a73ccda3": "https://caddyserver.com/docs/caddyfile/directives/reverse_proxy", - "s82efc39a8130": "https://caddyserver.com/docs/conventions", - "s5a6da5adf385": "https://caddyserver.com/docs/caddyfile/directives/header", - "s8df93f3e3d4c": "https://caddyserver.com/docs/caddyfile/directives/forward_auth", - "sf12ad47e81b5": "https://caddyserver.com/docs/command-line#signals", - "s38b1b78ce980": "https://caddyserver.com/docs/caddyfile/matchers" - } - }, - "letsencrypt-notice": { - "name": "Let's Encrypt expiration-email notice", - "basis": "2025", - "sources": { - "sdf68595235d4": "https://letsencrypt.org/2025/01/22/ending-expiration-emails/" - } - }, - "service": { - "name": "caddyserver/dist service unit", - "basis": "33ae08ff08d168572df2956ed14fbc4949880d94", - "sources": { - "se70d276b03a1": "https://github.com/caddyserver/dist/blob/33ae08ff08d168572df2956ed14fbc4949880d94/init/caddy.service#L22-L32" - } - }, - "systemd": { - "name": "systemd manual", - "basis": "v262", - "sources": { - "see108b0e0bba": "https://github.com/systemd/systemd/blob/v262/man/systemd.exec.xml#L1558-L1660" - } - }, - "cloudflare": { - "name": "Cloudflare docs", - "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", - "sources": { - "s6f6104362214": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L17-L85", - "s81e5dd28d3e5": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json.mdx#L15-L17", - "sa7c8d635bfd5": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/partials/cloudflare-one/tunnel/origin-parameters.mdx#L196-L215" - } - }, - "ratelimit-source": { - "name": "caddy-ratelimit source", - "basis": "5625512f24f6f59d6f64fb3aafe5eecff0b286db", - "sources": { - "s093420cd3d2f": "https://raw.githubusercontent.com/mholt/caddy-ratelimit/5625512f24f6f59d6f64fb3aafe5eecff0b286db/README.md" - } - } - }, - "claims": { - "auto-https": {"text": "Caddy 2 obtains and renews public certificates and redirects HTTP to HTTPS; DNS must point here and the example requires public 80/443.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, - "issuers": {"text": "Public certificates come from Let's Encrypt or ZeroSSL and renew automatically.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, - "renewal-monitoring": {"text": "Set the ACME email contact but monitor renewal yourself: Let's Encrypt stopped expiration emails.", "components": ["docs-rolling", "letsencrypt-notice"], "sources": ["docs-rolling:sdbcd496895d6", "letsencrypt-notice:sdf68595235d4"], "status": "REASONED"}, - "backend": {"text": "reverse_proxy sends traffic to 127.0.0.1:3000; keep the app private to prevent TLS/auth bypass.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED"}, - "hsts": {"text": "Caddy sends no HSTS automatically; add it only after HTTPS works across every included subdomain.", "components": ["docs-rolling"], "sources": ["docs-rolling:s5a6da5adf385", "docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, - "forwarded": {"text": "Caddy ignores untrusted client X-Forwarded-* by default; configure trusted_proxies behind a trusted CDN/proxy.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED"}, - "internal-tls": {"text": "tls internal uses a local CA; clients must trust its root, with caddy trust for the local host. tls also accepts explicit cert/key files.", "components": ["docs-rolling"], "sources": ["docs-rolling:s2e47c3139bd2", "docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, - "basic": {"text": "basic_auth uses hashed passwords from caddy hash-password; it was named basicauth before v2.8.0 and remains single-factor.", "components": ["docs-rolling"], "sources": ["docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, - "path-auth": {"text": "Protect /admin and /admin/* together: path matches are exact and multiple paths are OR'ed.", "components": ["docs-rolling"], "sources": ["docs-rolling:s38b1b78ce980", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, - "mfa": {"text": "forward_auth (Caddy 2.5+) can delegate human authentication to Authelia; Cloudflare Access is a proposed layer that enforces MFA when its governing Access setting (policy over application over organization) requires it.", "components": ["docs-rolling", "cloudflare"], "sources": ["docs-rolling:s8df93f3e3d4c", "cloudflare:s6f6104362214"], "status": "REASONED"}, - "access-origin": {"text": "Cloudflare Access can require MFA when configured; the guide reasons that direct origin ingress can bypass Access, so restrict it; validate the Access JWT at the origin, which a tunnel's `access` setting has `cloudflared` do. Caddy still answers directly on 443.", "components": ["docs-rolling", "cloudflare"], "sources": ["docs-rolling:s3865a73ccda3", "cloudflare:s6f6104362214", "cloudflare:s81e5dd28d3e5", "cloudflare:sa7c8d635bfd5"], "status": "REASONED"}, - "body-limit": {"text": "Add request_body max_size 10MB inside the existing authenticated site; replacing the site without basic_auth makes it public.", "components": ["docs-rolling"], "sources": ["docs-rolling:s31af5ed45a20", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, - "rate-limit": {"text": "The standard build has no rate_limit directive; use a compiled community caddy-ratelimit module or an upstream layer.", "components": ["docs-rolling", "ratelimit-source"], "sources": ["docs-rolling:sec59a61934c5", "ratelimit-source:s093420cd3d2f"], "status": "REASONED"}, - "admin-bind": {"text": "The admin API defaults to localhost:2019 as of v2.11.4; never publish it or reverse-proxy to it.", "components": ["admin", "docs-rolling"], "sources": ["admin:s35044463fa67", "admin:s2bbcf239df02", "docs-rolling:s3923477355fb"], "status": "REASONED"}, - "admin-authority": {"text": "Reachable unauthenticated admin clients can replace config at POST /load, edit /config/ or stop with POST /stop.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb"], "status": "REASONED"}, - "admin-isolation": {"text": "Host/Origin checks are not process isolation; local untrusted workloads require a permissioned Unix socket.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "docs-rolling:sdbcd496895d6"], "status": "REASONED"}, - "docker-admin": {"text": "The official 2.11.4 Alpine, Windows Server Core 2022/2025, and Nano Server 2022/2025 runtime tags map to fba28535 Dockerfiles that EXPOSE 2019 and run the default Caddyfile without admin; absent overrides, the endpoint stays namespace-local.", "components": ["tags", "images", "dist", "admin"], "sources": ["tags:sdc8aca42f5e2", "images:s0f0994ef9fac", "images:s2dce0bbf3a70", "images:sd2c1b05a8088", "images:sf3e9c2d4e0cd", "images:s6c800a156eac", "images:sc5c82704d33f", "images:sc1cb0e1fb9eb", "images:s11528dfcc516", "images:s70b76475a281", "dist:s433758a90af0", "admin:s35044463fa67", "admin:s2bbcf239df02", "images:sc5dd3f9746fe"], "status": "REASONED"}, - "admin-override": {"text": "A mounted admin setting or CADDY_ADMIN changes the address; do not set 0.0.0.0:2019 to make publication work.", "components": ["admin", "dist", "docs-rolling"], "sources": ["docs-rolling:sdbcd496895d6", "admin:s35044463fa67", "admin:s2bbcf239df02", "dist:s433758a90af0"], "status": "REASONED"}, - "namespace": {"text": "The shipped admin default is localhost:2019. On Docker Engine on Linux, bridge networking keeps loopback container-local; host or container networking shares a namespace, so treat its processes as potential admin clients.", "components": ["images", "dist", "admin", "docker-network", "docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "images:s0f0994ef9fac", "images:s2dce0bbf3a70", "images:sd2c1b05a8088", "images:sf3e9c2d4e0cd", "images:sc5dd3f9746fe", "images:s6c800a156eac", "images:sc5c82704d33f", "images:sc1cb0e1fb9eb", "images:s11528dfcc516", "images:s70b76475a281", "dist:s433758a90af0", "admin:s35044463fa67", "docker-network:se1437c0f19fb", "docker-network:sfce645f8b46e", "docker-network:sfd29f33e4108"], "status": "REASONED"}, - "admin-socket": {"text": "admin unix//run/caddy/admin.sock uses owner-only mode 0200 by default; append |0600 to set it explicitly.", "components": ["docs-rolling"], "sources": ["docs-rolling:sdbcd496895d6", "docs-rolling:s82efc39a8130"], "status": "REASONED"}, - "runtime-directory": {"text": "The dist service at 33ae08ff runs as caddy without RuntimeDirectory; systemd v262 creates /run/caddy for RuntimeDirectory=caddy at service start and owns it as caddy. Restart for the socket switch is guide reasoning.", "components": ["docs-rolling", "service", "systemd"], "sources": ["docs-rolling:sdbcd496895d6", "service:se70d276b03a1", "systemd:see108b0e0bba"], "status": "REASONED"}, - "reload": {"text": "The dist service at 33ae08ff uses caddy reload --config /etc/caddy/Caddyfile --force; API reload uses the Caddyfile admin address. With admin off, guide reasoning requires systemd restart.", "components": ["docs-rolling", "service"], "sources": ["docs-rolling:sdbcd496895d6", "docs-rolling:sf12ad47e81b5", "service:se70d276b03a1"], "status": "REASONED"}, - "signal-reload": {"text": "A manual run without --resume may reload startup config via SIGUSR1; API changes or reload with a different file/adapter disable signal reloads.", "components": ["docs-rolling"], "sources": ["docs-rolling:sf12ad47e81b5"], "status": "REASONED"}, - "on-demand": {"text": "Do not enable unrestricted on_demand TLS without an ask endpoint or host allowlist; this guide does not enable it.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, - "verify-config": {"text": "Validate /etc/caddy/Caddyfile; no validation outcome is recorded.", "components": ["docs-rolling"], "sources": ["docs-rolling:sf12ad47e81b5"], "status": "REASONED", "verify": [1]}, - "verify-redirect": {"text": "HTTP should redirect to HTTPS.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED", "verify": [1]}, - "verify-auth": {"text": "With site-wide auth, HTTPS without credentials returns 401.", "components": ["docs-rolling"], "sources": ["docs-rolling:sbc2bd9599bd1"], "status": "REASONED", "verify": [1]}, - "verify-scope": {"text": "The @admin variant must return 401 for /admin and /admin/x, but an app response rather than 401 for a non-admin path.", "components": ["docs-rolling"], "sources": ["docs-rolling:s38b1b78ce980", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED", "verify": [1]}, - "verify-size": {"text": "Authenticated 1M reaches the app and 11M returns 413; 401/000 void the control. Auth can prevent body reads; remove request_body in isolation to attribute refusal.", "components": ["docs-rolling"], "sources": ["docs-rolling:s31af5ed45a20", "docs-rolling:sbc2bd9599bd1", "docs-rolling:sec59a61934c5"], "status": "REASONED", "verify": [1]}, - "verify-backend": {"text": "Read all listeners: app 3000 must be loopback-only even if proxy checks pass.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED", "verify": [1]}, - "verify-admin": {"text": "TCP admin must bind only loopback; absent 2019 requires confirming admin off or a socket. Check Unix socket existence and owner restrictions separately.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "docs-rolling:sdbcd496895d6", "docs-rolling:s82efc39a8130"], "status": "REASONED", "verify": [1]} - } -} ---- # Caddy: TLS and authentication - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +Written against Caddy admin source v2.11.4, systemd manual v262, and Let's Encrypt expiration-email notice 2025; 7 further sources are pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-09-27; documentation checked September 2026. The guide records 31 claims, all reasoned from cited sources. -Metadata reviewed 2026-09-27; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- admin: Caddy admin source (v2.11.4) -- tags: Official image tag map (d82ca5102fa6735be29d5e1fc6ce03af77eb091e) -- images: Caddy Docker source (fba2853501d36e8a72f946ac8cb7ff64d07e48f2) -- dist: Default Caddyfile source (33ae08ff08d168572df2956ed14fbc4949880d94) -- docker-network: Docker networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- docs-rolling: Caddy documentation (rolling) (unknown) -- letsencrypt-notice: Let's Encrypt expiration-email notice (2025) -- service: caddyserver/dist service unit (33ae08ff08d168572df2956ed14fbc4949880d94) -- systemd: systemd manual (v262) -- cloudflare: Cloudflare docs (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- ratelimit-source: caddy-ratelimit source (5625512f24f6f59d6f64fb3aafe5eecff0b286db) - -Claims (full source mapping in the front matter): - -- auto-https (REASONED; docs-rolling): Caddy 2 obtains and renews public certificates and redirects HTTP to HTTPS; DNS must point here and the example requires public 80/443. -- issuers (REASONED; docs-rolling): Public certificates come from Let's Encrypt or ZeroSSL and renew automatically. -- renewal-monitoring (REASONED; docs-rolling, letsencrypt-notice): Set the ACME email contact but monitor renewal yourself: Let's Encrypt stopped expiration emails. -- backend (REASONED; docs-rolling): reverse_proxy sends traffic to 127.0.0.1:3000; keep the app private to prevent TLS/auth bypass. -- hsts (REASONED; docs-rolling): Caddy sends no HSTS automatically; add it only after HTTPS works across every included subdomain. -- forwarded (REASONED; docs-rolling): Caddy ignores untrusted client X-Forwarded-* by default; configure trusted_proxies behind a trusted CDN/proxy. -- internal-tls (REASONED; docs-rolling): tls internal uses a local CA; clients must trust its root, with caddy trust for the local host. tls also accepts explicit cert/key files. -- basic (REASONED; docs-rolling): basic_auth uses hashed passwords from caddy hash-password; it was named basicauth before v2.8.0 and remains single-factor. -- path-auth (REASONED; docs-rolling): Protect /admin and /admin/* together: path matches are exact and multiple paths are OR'ed. -- mfa (REASONED; docs-rolling, cloudflare): forward_auth (Caddy 2.5+) can delegate human authentication to Authelia; Cloudflare Access is a proposed layer that enforces MFA when its governing Access setting (policy over application over organization) requires it. -- access-origin (REASONED; docs-rolling, cloudflare): Cloudflare Access can require MFA when configured; the guide reasons that direct origin ingress can bypass Access, so restrict it; validate the Access JWT at the origin, which a tunnel's `access` setting has `cloudflared` do. Caddy still answers directly on 443. -- body-limit (REASONED; docs-rolling): Add request_body max_size 10MB inside the existing authenticated site; replacing the site without basic_auth makes it public. -- rate-limit (REASONED; docs-rolling, ratelimit-source): The standard build has no rate_limit directive; use a compiled community caddy-ratelimit module or an upstream layer. -- admin-bind (REASONED; admin, docs-rolling): The admin API defaults to localhost:2019 as of v2.11.4; never publish it or reverse-proxy to it. -- admin-authority (REASONED; docs-rolling): Reachable unauthenticated admin clients can replace config at POST /load, edit /config/ or stop with POST /stop. -- admin-isolation (REASONED; docs-rolling): Host/Origin checks are not process isolation; local untrusted workloads require a permissioned Unix socket. -- docker-admin (REASONED; tags, images, dist, admin): The official 2.11.4 Alpine, Windows Server Core 2022/2025, and Nano Server 2022/2025 runtime tags map to fba28535 Dockerfiles that EXPOSE 2019 and run the default Caddyfile without admin; absent overrides, the endpoint stays namespace-local. -- admin-override (REASONED; admin, dist, docs-rolling): A mounted admin setting or CADDY_ADMIN changes the address; do not set 0.0.0.0:2019 to make publication work. -- namespace (REASONED; images, dist, admin, docker-network, docs-rolling): The shipped admin default is localhost:2019. On Docker Engine on Linux, bridge networking keeps loopback container-local; host or container networking shares a namespace, so treat its processes as potential admin clients. -- admin-socket (REASONED; docs-rolling): admin unix//run/caddy/admin.sock uses owner-only mode 0200 by default; append |0600 to set it explicitly. -- runtime-directory (REASONED; docs-rolling, service, systemd): The dist service at 33ae08ff runs as caddy without RuntimeDirectory; systemd v262 creates /run/caddy for RuntimeDirectory=caddy at service start and owns it as caddy. Restart for the socket switch is guide reasoning. -- reload (REASONED; docs-rolling, service): The dist service at 33ae08ff uses caddy reload --config /etc/caddy/Caddyfile --force; API reload uses the Caddyfile admin address. With admin off, guide reasoning requires systemd restart. -- signal-reload (REASONED; docs-rolling): A manual run without --resume may reload startup config via SIGUSR1; API changes or reload with a different file/adapter disable signal reloads. -- on-demand (REASONED; docs-rolling): Do not enable unrestricted on_demand TLS without an ask endpoint or host allowlist; this guide does not enable it. -- verify-config (REASONED; docs-rolling): Validate /etc/caddy/Caddyfile; no validation outcome is recorded. -- verify-redirect (REASONED; docs-rolling): HTTP should redirect to HTTPS. -- verify-auth (REASONED; docs-rolling): With site-wide auth, HTTPS without credentials returns 401. -- verify-scope (REASONED; docs-rolling): The @admin variant must return 401 for /admin and /admin/x, but an app response rather than 401 for a non-admin path. -- verify-size (REASONED; docs-rolling): Authenticated 1M reaches the app and 11M returns 413; 401/000 void the control. Auth can prevent body reads; remove request_body in isolation to attribute refusal. -- verify-backend (REASONED; docs-rolling): Read all listeners: app 3000 must be loopback-only even if proxy checks pass. -- verify-admin (REASONED; docs-rolling): TCP admin must bind only loopback; absent 2019 requires confirming admin off or a socket. Check Unix socket existence and owner restrictions separately. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Caddy 2 obtains, installs, and renews publicly trusted certificates automatically and redirects HTTP to HTTPS by default. For a new deployment with a public domain, it is the shortest correct path to HTTPS: no ACME client, no renewal timer, no redirect block. @@ -397,3 +199,227 @@ ls -l /run/caddy/admin.sock # if you bound it to a unix socket: it exis - Cloudflare Access JWT validation (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104, checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json.mdx#L15-L17 - Cloudflare Tunnel `access` setting, `cloudflared` validating the Access JWT (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104, checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/partials/cloudflare-one/tunnel/origin-parameters.mdx#L196-L215 - Docker networking documentation at 4e9a5751518ed8223a8dcde53693badddd72604f (container loopback, host namespace sharing, and the `--network container:` loopback example; the Linux bridge/host distinction above is inferred from these documented namespace semantics): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L286, https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/drivers/host.md#L11-L13, https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L302-L325 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/caddy.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- admin: Caddy admin source (v2.11.4) +- images: Caddy Docker source (fba2853501d36e8a72f946ac8cb7ff64d07e48f2) +- dist: Default Caddyfile source (33ae08ff08d168572df2956ed14fbc4949880d94) +- docs-rolling: Caddy documentation (rolling) (unknown) +- service: caddyserver/dist service unit (33ae08ff08d168572df2956ed14fbc4949880d94) +- ratelimit-source: caddy-ratelimit source (5625512f24f6f59d6f64fb3aafe5eecff0b286db) +- tags: Official image tag map (d82ca5102fa6735be29d5e1fc6ce03af77eb091e) +- docker-network: Docker networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- systemd: systemd manual (v262) +- cloudflare: Cloudflare docs (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- letsencrypt-notice: Let's Encrypt expiration-email notice (2025) + +Claims (full source mapping in the JSON block below): + +- auto-https (REASONED; docs-rolling): Caddy 2 obtains and renews public certificates and redirects HTTP to HTTPS; DNS must point here and the example requires public 80/443. +- issuers (REASONED; docs-rolling): Public certificates come from Let's Encrypt or ZeroSSL and renew automatically. +- renewal-monitoring (REASONED; docs-rolling, letsencrypt-notice): Set the ACME email contact but monitor renewal yourself: Let's Encrypt stopped expiration emails. +- backend (REASONED; docs-rolling): reverse_proxy sends traffic to 127.0.0.1:3000; keep the app private to prevent TLS/auth bypass. +- hsts (REASONED; docs-rolling): Caddy sends no HSTS automatically; add it only after HTTPS works across every included subdomain. +- forwarded (REASONED; docs-rolling): Caddy ignores untrusted client X-Forwarded-* by default; configure trusted_proxies behind a trusted CDN/proxy. +- internal-tls (REASONED; docs-rolling): tls internal uses a local CA; clients must trust its root, with caddy trust for the local host. tls also accepts explicit cert/key files. +- basic (REASONED; docs-rolling): basic_auth uses hashed passwords from caddy hash-password; it was named basicauth before v2.8.0 and remains single-factor. +- path-auth (REASONED; docs-rolling): Protect /admin and /admin/* together: path matches are exact and multiple paths are OR'ed. +- mfa (REASONED; docs-rolling, cloudflare): forward_auth (Caddy 2.5+) can delegate human authentication to Authelia; Cloudflare Access is a proposed layer that enforces MFA when its governing Access setting (policy over application over organization) requires it. +- access-origin (REASONED; docs-rolling, cloudflare): Cloudflare Access can require MFA when configured; the guide reasons that direct origin ingress can bypass Access, so restrict it; validate the Access JWT at the origin, which a tunnel's `access` setting has `cloudflared` do. Caddy still answers directly on 443. +- body-limit (REASONED; docs-rolling): Add request_body max_size 10MB inside the existing authenticated site; replacing the site without basic_auth makes it public. +- rate-limit (REASONED; docs-rolling, ratelimit-source): The standard build has no rate_limit directive; use a compiled community caddy-ratelimit module or an upstream layer. +- admin-bind (REASONED; admin, docs-rolling): The admin API defaults to localhost:2019 as of v2.11.4; never publish it or reverse-proxy to it. +- admin-authority (REASONED; docs-rolling): Reachable unauthenticated admin clients can replace config at POST /load, edit /config/ or stop with POST /stop. +- admin-isolation (REASONED; docs-rolling): Host/Origin checks are not process isolation; local untrusted workloads require a permissioned Unix socket. +- docker-admin (REASONED; tags, images, dist, admin): The official 2.11.4 Alpine, Windows Server Core 2022/2025, and Nano Server 2022/2025 runtime tags map to fba28535 Dockerfiles that EXPOSE 2019 and run the default Caddyfile without admin; absent overrides, the endpoint stays namespace-local. +- admin-override (REASONED; admin, dist, docs-rolling): A mounted admin setting or CADDY_ADMIN changes the address; do not set 0.0.0.0:2019 to make publication work. +- namespace (REASONED; images, dist, admin, docker-network, docs-rolling): The shipped admin default is localhost:2019. On Docker Engine on Linux, bridge networking keeps loopback container-local; host or container networking shares a namespace, so treat its processes as potential admin clients. +- admin-socket (REASONED; docs-rolling): admin unix//run/caddy/admin.sock uses owner-only mode 0200 by default; append |0600 to set it explicitly. +- runtime-directory (REASONED; docs-rolling, service, systemd): The dist service at 33ae08ff runs as caddy without RuntimeDirectory; systemd v262 creates /run/caddy for RuntimeDirectory=caddy at service start and owns it as caddy. Restart for the socket switch is guide reasoning. +- reload (REASONED; docs-rolling, service): The dist service at 33ae08ff uses caddy reload --config /etc/caddy/Caddyfile --force; API reload uses the Caddyfile admin address. With admin off, guide reasoning requires systemd restart. +- signal-reload (REASONED; docs-rolling): A manual run without --resume may reload startup config via SIGUSR1; API changes or reload with a different file/adapter disable signal reloads. +- on-demand (REASONED; docs-rolling): Do not enable unrestricted on_demand TLS without an ask endpoint or host allowlist; this guide does not enable it. +- verify-config (REASONED; docs-rolling): Validate /etc/caddy/Caddyfile; no validation outcome is recorded. +- verify-redirect (REASONED; docs-rolling): HTTP should redirect to HTTPS. +- verify-auth (REASONED; docs-rolling): With site-wide auth, HTTPS without credentials returns 401. +- verify-scope (REASONED; docs-rolling): The @admin variant must return 401 for /admin and /admin/x, but an app response rather than 401 for a non-admin path. +- verify-size (REASONED; docs-rolling): Authenticated 1M reaches the app and 11M returns 413; 401/000 void the control. Auth can prevent body reads; remove request_body in isolation to attribute refusal. +- verify-backend (REASONED; docs-rolling): Read all listeners: app 3000 must be loopback-only even if proxy checks pass. +- verify-admin (REASONED; docs-rolling): TCP admin must bind only loopback; absent 2019 requires confirming admin off or a socket. Check Unix socket existence and owner restrictions separately. + +````json +{ + "schema": 1, + "checked": "2026-09-27", + "documentation_checked": "2026-09", + "body_sha256": "7a5caab2cbcf90c693eab11084c5f8aebc6a3bc8e613f7ab76ca8167e3a9ea28", + "components": { + "admin": { + "name": "Caddy admin source", + "basis": "v2.11.4", + "sources": { + "s35044463fa67": "https://github.com/caddyserver/caddy/blob/v2.11.4/admin.go#L1433", + "s2bbcf239df02": "https://github.com/caddyserver/caddy/blob/v2.11.4/admin.go#L58-L67" + } + }, + "images": { + "name": "Caddy Docker source", + "basis": "fba2853501d36e8a72f946ac8cb7ff64d07e48f2", + "sources": { + "s0f0994ef9fac": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L17", + "s2dce0bbf3a70": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L59-L63", + "sd2c1b05a8088": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2022/Dockerfile#L27-L32", + "sf3e9c2d4e0cd": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2022/Dockerfile#L8", + "sc5dd3f9746fe": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2025/Dockerfile#L27-L32", + "s6c800a156eac": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2025/Dockerfile#L8", + "sc5c82704d33f": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2022/Dockerfile#L10", + "sc1cb0e1fb9eb": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2022/Dockerfile#L42-L47", + "s11528dfcc516": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2025/Dockerfile#L10", + "s70b76475a281": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2025/Dockerfile#L42-L47" + } + }, + "dist": { + "name": "Default Caddyfile source", + "basis": "33ae08ff08d168572df2956ed14fbc4949880d94", + "sources": { + "s433758a90af0": "https://github.com/caddyserver/dist/blob/33ae08ff08d168572df2956ed14fbc4949880d94/config/Caddyfile" + } + }, + "docs-rolling": { + "name": "Caddy documentation (rolling)", + "basis": "unknown", + "sources": { + "s434e7b7d7f63": "https://caddyserver.com/docs/automatic-https", + "s3923477355fb": "https://caddyserver.com/docs/api", + "sdbcd496895d6": "https://caddyserver.com/docs/caddyfile/options", + "s31af5ed45a20": "https://caddyserver.com/docs/caddyfile/directives/request_body", + "sec59a61934c5": "https://caddyserver.com/docs/caddyfile/directives", + "sbc2bd9599bd1": "https://caddyserver.com/docs/caddyfile/directives/basic_auth", + "s2e47c3139bd2": "https://caddyserver.com/docs/caddyfile/directives/tls", + "s3865a73ccda3": "https://caddyserver.com/docs/caddyfile/directives/reverse_proxy", + "s82efc39a8130": "https://caddyserver.com/docs/conventions", + "s5a6da5adf385": "https://caddyserver.com/docs/caddyfile/directives/header", + "s8df93f3e3d4c": "https://caddyserver.com/docs/caddyfile/directives/forward_auth", + "sf12ad47e81b5": "https://caddyserver.com/docs/command-line#signals", + "s38b1b78ce980": "https://caddyserver.com/docs/caddyfile/matchers" + } + }, + "service": { + "name": "caddyserver/dist service unit", + "basis": "33ae08ff08d168572df2956ed14fbc4949880d94", + "sources": { + "se70d276b03a1": "https://github.com/caddyserver/dist/blob/33ae08ff08d168572df2956ed14fbc4949880d94/init/caddy.service#L22-L32" + } + }, + "ratelimit-source": { + "name": "caddy-ratelimit source", + "basis": "5625512f24f6f59d6f64fb3aafe5eecff0b286db", + "sources": { + "s093420cd3d2f": "https://raw.githubusercontent.com/mholt/caddy-ratelimit/5625512f24f6f59d6f64fb3aafe5eecff0b286db/README.md" + } + }, + "tags": { + "name": "Official image tag map", + "basis": "d82ca5102fa6735be29d5e1fc6ce03af77eb091e", + "sources": { + "sdc8aca42f5e2": "https://github.com/docker-library/official-images/blob/d82ca5102fa6735be29d5e1fc6ce03af77eb091e/library/caddy#L7-L65" + } + }, + "docker-network": { + "name": "Docker networking documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "se1437c0f19fb": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L286", + "sfce645f8b46e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/drivers/host.md#L11-L13", + "sfd29f33e4108": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L302-L325" + } + }, + "systemd": { + "name": "systemd manual", + "basis": "v262", + "sources": { + "see108b0e0bba": "https://github.com/systemd/systemd/blob/v262/man/systemd.exec.xml#L1558-L1660" + } + }, + "cloudflare": { + "name": "Cloudflare docs", + "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "s6f6104362214": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L17-L85", + "s81e5dd28d3e5": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json.mdx#L15-L17", + "sa7c8d635bfd5": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/partials/cloudflare-one/tunnel/origin-parameters.mdx#L196-L215" + } + }, + "letsencrypt-notice": { + "name": "Let's Encrypt expiration-email notice", + "basis": "2025", + "sources": { + "sdf68595235d4": "https://letsencrypt.org/2025/01/22/ending-expiration-emails/" + } + } + }, + "claims": { + "auto-https": {"text": "Caddy 2 obtains and renews public certificates and redirects HTTP to HTTPS; DNS must point here and the example requires public 80/443.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "issuers": {"text": "Public certificates come from Let's Encrypt or ZeroSSL and renew automatically.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "renewal-monitoring": {"text": "Set the ACME email contact but monitor renewal yourself: Let's Encrypt stopped expiration emails.", "components": ["docs-rolling", "letsencrypt-notice"], "sources": ["docs-rolling:sdbcd496895d6", "letsencrypt-notice:sdf68595235d4"], "status": "REASONED"}, + "backend": {"text": "reverse_proxy sends traffic to 127.0.0.1:3000; keep the app private to prevent TLS/auth bypass.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED"}, + "hsts": {"text": "Caddy sends no HSTS automatically; add it only after HTTPS works across every included subdomain.", "components": ["docs-rolling"], "sources": ["docs-rolling:s5a6da5adf385", "docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "forwarded": {"text": "Caddy ignores untrusted client X-Forwarded-* by default; configure trusted_proxies behind a trusted CDN/proxy.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED"}, + "internal-tls": {"text": "tls internal uses a local CA; clients must trust its root, with caddy trust for the local host. tls also accepts explicit cert/key files.", "components": ["docs-rolling"], "sources": ["docs-rolling:s2e47c3139bd2", "docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "basic": {"text": "basic_auth uses hashed passwords from caddy hash-password; it was named basicauth before v2.8.0 and remains single-factor.", "components": ["docs-rolling"], "sources": ["docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, + "path-auth": {"text": "Protect /admin and /admin/* together: path matches are exact and multiple paths are OR'ed.", "components": ["docs-rolling"], "sources": ["docs-rolling:s38b1b78ce980", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, + "mfa": {"text": "forward_auth (Caddy 2.5+) can delegate human authentication to Authelia; Cloudflare Access is a proposed layer that enforces MFA when its governing Access setting (policy over application over organization) requires it.", "components": ["docs-rolling", "cloudflare"], "sources": ["docs-rolling:s8df93f3e3d4c", "cloudflare:s6f6104362214"], "status": "REASONED"}, + "access-origin": {"text": "Cloudflare Access can require MFA when configured; the guide reasons that direct origin ingress can bypass Access, so restrict it; validate the Access JWT at the origin, which a tunnel's `access` setting has `cloudflared` do. Caddy still answers directly on 443.", "components": ["docs-rolling", "cloudflare"], "sources": ["docs-rolling:s3865a73ccda3", "cloudflare:s6f6104362214", "cloudflare:s81e5dd28d3e5", "cloudflare:sa7c8d635bfd5"], "status": "REASONED"}, + "body-limit": {"text": "Add request_body max_size 10MB inside the existing authenticated site; replacing the site without basic_auth makes it public.", "components": ["docs-rolling"], "sources": ["docs-rolling:s31af5ed45a20", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, + "rate-limit": {"text": "The standard build has no rate_limit directive; use a compiled community caddy-ratelimit module or an upstream layer.", "components": ["docs-rolling", "ratelimit-source"], "sources": ["docs-rolling:sec59a61934c5", "ratelimit-source:s093420cd3d2f"], "status": "REASONED"}, + "admin-bind": {"text": "The admin API defaults to localhost:2019 as of v2.11.4; never publish it or reverse-proxy to it.", "components": ["admin", "docs-rolling"], "sources": ["admin:s35044463fa67", "admin:s2bbcf239df02", "docs-rolling:s3923477355fb"], "status": "REASONED"}, + "admin-authority": {"text": "Reachable unauthenticated admin clients can replace config at POST /load, edit /config/ or stop with POST /stop.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb"], "status": "REASONED"}, + "admin-isolation": {"text": "Host/Origin checks are not process isolation; local untrusted workloads require a permissioned Unix socket.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "docs-rolling:sdbcd496895d6"], "status": "REASONED"}, + "docker-admin": {"text": "The official 2.11.4 Alpine, Windows Server Core 2022/2025, and Nano Server 2022/2025 runtime tags map to fba28535 Dockerfiles that EXPOSE 2019 and run the default Caddyfile without admin; absent overrides, the endpoint stays namespace-local.", "components": ["tags", "images", "dist", "admin"], "sources": ["tags:sdc8aca42f5e2", "images:s0f0994ef9fac", "images:s2dce0bbf3a70", "images:sd2c1b05a8088", "images:sf3e9c2d4e0cd", "images:s6c800a156eac", "images:sc5c82704d33f", "images:sc1cb0e1fb9eb", "images:s11528dfcc516", "images:s70b76475a281", "dist:s433758a90af0", "admin:s35044463fa67", "admin:s2bbcf239df02", "images:sc5dd3f9746fe"], "status": "REASONED"}, + "admin-override": {"text": "A mounted admin setting or CADDY_ADMIN changes the address; do not set 0.0.0.0:2019 to make publication work.", "components": ["admin", "dist", "docs-rolling"], "sources": ["docs-rolling:sdbcd496895d6", "admin:s35044463fa67", "admin:s2bbcf239df02", "dist:s433758a90af0"], "status": "REASONED"}, + "namespace": {"text": "The shipped admin default is localhost:2019. On Docker Engine on Linux, bridge networking keeps loopback container-local; host or container networking shares a namespace, so treat its processes as potential admin clients.", "components": ["images", "dist", "admin", "docker-network", "docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "images:s0f0994ef9fac", "images:s2dce0bbf3a70", "images:sd2c1b05a8088", "images:sf3e9c2d4e0cd", "images:sc5dd3f9746fe", "images:s6c800a156eac", "images:sc5c82704d33f", "images:sc1cb0e1fb9eb", "images:s11528dfcc516", "images:s70b76475a281", "dist:s433758a90af0", "admin:s35044463fa67", "docker-network:se1437c0f19fb", "docker-network:sfce645f8b46e", "docker-network:sfd29f33e4108"], "status": "REASONED"}, + "admin-socket": {"text": "admin unix//run/caddy/admin.sock uses owner-only mode 0200 by default; append |0600 to set it explicitly.", "components": ["docs-rolling"], "sources": ["docs-rolling:sdbcd496895d6", "docs-rolling:s82efc39a8130"], "status": "REASONED"}, + "runtime-directory": {"text": "The dist service at 33ae08ff runs as caddy without RuntimeDirectory; systemd v262 creates /run/caddy for RuntimeDirectory=caddy at service start and owns it as caddy. Restart for the socket switch is guide reasoning.", "components": ["docs-rolling", "service", "systemd"], "sources": ["docs-rolling:sdbcd496895d6", "service:se70d276b03a1", "systemd:see108b0e0bba"], "status": "REASONED"}, + "reload": {"text": "The dist service at 33ae08ff uses caddy reload --config /etc/caddy/Caddyfile --force; API reload uses the Caddyfile admin address. With admin off, guide reasoning requires systemd restart.", "components": ["docs-rolling", "service"], "sources": ["docs-rolling:sdbcd496895d6", "docs-rolling:sf12ad47e81b5", "service:se70d276b03a1"], "status": "REASONED"}, + "signal-reload": {"text": "A manual run without --resume may reload startup config via SIGUSR1; API changes or reload with a different file/adapter disable signal reloads.", "components": ["docs-rolling"], "sources": ["docs-rolling:sf12ad47e81b5"], "status": "REASONED"}, + "on-demand": {"text": "Do not enable unrestricted on_demand TLS without an ask endpoint or host allowlist; this guide does not enable it.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "verify-config": {"text": "Validate /etc/caddy/Caddyfile; no validation outcome is recorded.", "components": ["docs-rolling"], "sources": ["docs-rolling:sf12ad47e81b5"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP should redirect to HTTPS.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "With site-wide auth, HTTPS without credentials returns 401.", "components": ["docs-rolling"], "sources": ["docs-rolling:sbc2bd9599bd1"], "status": "REASONED", "verify": [1]}, + "verify-scope": {"text": "The @admin variant must return 401 for /admin and /admin/x, but an app response rather than 401 for a non-admin path.", "components": ["docs-rolling"], "sources": ["docs-rolling:s38b1b78ce980", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED", "verify": [1]}, + "verify-size": {"text": "Authenticated 1M reaches the app and 11M returns 413; 401/000 void the control. Auth can prevent body reads; remove request_body in isolation to attribute refusal.", "components": ["docs-rolling"], "sources": ["docs-rolling:s31af5ed45a20", "docs-rolling:sbc2bd9599bd1", "docs-rolling:sec59a61934c5"], "status": "REASONED", "verify": [1]}, + "verify-backend": {"text": "Read all listeners: app 3000 must be loopback-only even if proxy checks pass.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED", "verify": [1]}, + "verify-admin": {"text": "TCP admin must bind only loopback; absent 2019 requires confirming admin off or a socket. Check Unix socket existence and owner restrictions separately.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "docs-rolling:sdbcd496895d6", "docs-rolling:s82efc39a8130"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/container-hardening.md b/container-hardening.md index 9a3f505cb..8fa6a1dcf 100644 --- a/container-hardening.md +++ b/container-hardening.md @@ -1,173 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-18", - "documentation_checked": "2026-09", - "body_sha256": "096aac84d8e85e741360a95b6e4fbadf6b3621a153725920785f898bb479528b", - "components": { - "docker": { - "name": "Docker documentation", - "basis": "unknown", - "sources": { - "s6ca35cbd69e7": "https://docs.docker.com/reference/dockerfile/", - "s8b7485a246e4": "https://docs.docker.com/reference/compose-file/", - "s5d6294cf605e": "https://docs.docker.com/reference/cli/docker/container/run/" - } - }, - "docker-daemon": { - "name": "Docker daemon security documentation", - "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", - "sources": { - "secebea1a69ab": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/_index.md#L84-L112", - "s79490cd2f3b2": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/install/linux-postinstall.md#L29-L39", - "s8eab393a61c6": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/rootless/_index.md#L8-L21", - "s94d57fcbfafe": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1330-L1350", - "s372ddf9da1f0": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1460-L1467", - "see4d326cd914": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1495-L1515" - } - }, - "compose-2324": { - "name": "Docker Compose source and reference", - "basis": "v2.32.4", - "sources": { - "s906b5822e9c4": "https://github.com/docker/compose/blob/v2.32.4/docs/reference/compose_exec.md#L1-L7", - "sfe4c568a9887": "https://github.com/docker/compose/blob/v2.32.4/cmd/compose/exec.go#L84-L110", - "s1bf114813e47": "https://github.com/docker/compose/blob/v2.32.4/cmd/compose/up.go#L145-L155" - } - }, - "procfs-619": { - "name": "Linux process status and capabilities manuals", - "basis": "man-pages-6.19", - "sources": { - "sa94440d59945": "https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man/man5/proc_pid_status.5?h=man-pages-6.19#n315", - "s74035196d5f7": "https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man/man7/capabilities.7?h=man-pages-6.19#n1367" - } - }, - "kubernetes-website": { - "name": "Kubernetes process namespace documentation", - "basis": "47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8", - "sources": { - "s3df8336a2aa8": "https://github.com/kubernetes/website/blob/47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8/content/en/docs/tasks/configure-pod-container/share-process-namespace.md#L115-L119" - } - }, - "kubernetes": { - "name": "Kubernetes documentation", - "basis": "unknown", - "sources": { - "sb78ea91d0308": "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/", - "s1a2d717974c7": "https://kubernetes.io/docs/concepts/security/pod-security-standards/", - "s953450b4076f": "https://kubernetes.io/docs/concepts/services-networking/network-policies/" - } - }, - "kubernetes-134": { - "name": "Kubernetes Pod Security Admission", - "basis": "1.34", - "sources": { - "s9b9aaf5067e2": "https://v1-34.docs.kubernetes.io/docs/concepts/security/pod-security-admission/", - "sa25682f200f0": "https://v1-34.docs.kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/" - } - }, - "kubectl-134": { - "name": "kubectl source", - "basis": "v1.34.0", - "sources": { - "s0906dc78482b": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L293-L310", - "s1adb8cde69c0": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L103-L194", - "sc98b92ba3ebf": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L591-L622", - "s2ffc04de1d63": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/util/override_options.go#L29-L90", - "sf5417ce59241": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/exec/exec.go#L52-L56" - } - }, - "rfc7396": { - "name": "JSON Merge Patch", - "basis": "RFC 7396", - "sources": { - "sda39f95d0890": "https://www.rfc-editor.org/rfc/rfc7396#section-2" - } - }, - "busybox-1361": { - "name": "BusyBox source", - "basis": "1_36_1", - "sources": { - "s14b89daa008d": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc.c#L30-L59", - "s34bdee292f79": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc_bloaty.c#L61-L93", - "s8a53fb647996": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc_bloaty.c#L888-L922" - } - } - }, - "claims": { - "docker-user": {"text": "Dockerfile USER sets build/runtime identity; set user and group, since no primary group uses root. Compose user overrides it; unset in both runs as root.", "components": ["docker"], "sources": ["docker:s6ca35cbd69e7", "docker:s8b7485a246e4"], "status": "REASONED"}, - "docker-rootfs": {"text": "Compose read_only makes the root filesystem read-only; provide tmpfs only for required writable paths.", "components": ["docker"], "sources": ["docker:s8b7485a246e4"], "status": "REASONED"}, - "docker-capabilities": {"text": "cap_drop: [ALL] removes capabilities; add back only specific required capabilities with cap_add.", "components": ["docker"], "sources": ["docker:s8b7485a246e4"], "status": "REASONED"}, - "docker-escalation": {"text": "no-new-privileges prevents setuid privilege gain; the guide lists bare, equals-true and colon-true spellings as equivalent.", "components": ["docker"], "sources": ["docker:s5d6294cf605e", "docker:s8b7485a246e4"], "status": "REASONED"}, - "docker-socket": {"text": "Never mount /var/run/docker.sock. In Docker's default rootful configuration, daemon control lets a user start a container that can alter the host filesystem without restriction, and docker-group membership grants root-level privileges; Rootless mode runs the daemon and containers without root privileges. Docker documents mounted-socket daemon control with a Docker client, and documents --privileged and host bind mounts; a socket-holding container starting a privileged sibling or bind-mounting host / to escape is this guide's assessment. Rootless mode narrowing the available host privilege but not making the mount safe is this guide's assessment.", "components": ["docker", "docker-daemon"], "sources": ["docker:s5d6294cf605e", "docker-daemon:secebea1a69ab", "docker-daemon:s79490cd2f3b2", "docker-daemon:s8eab393a61c6", "docker-daemon:s94d57fcbfafe", "docker-daemon:s372ddf9da1f0", "docker-daemon:see4d326cd914"], "status": "REASONED"}, - "kube-user": {"text": "runAsNonRoot rejects root; pin runAsUser and runAsGroup to 10001, with the guide noting runtime-default GID 0 if unset.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308"], "status": "REASONED"}, - "kube-rootfs": {"text": "Set readOnlyRootFilesystem per container; this is a separate recommendation, not a restricted Pod Security requirement.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, - "kube-escalation": {"text": "Set allowPrivilegeEscalation: false in the container securityContext.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, - "kube-capabilities": {"text": "Drop ALL capabilities in the container securityContext.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, - "kube-seccomp": {"text": "RuntimeDefault selects the runtime syscall filter instead of Unconfined.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, - "admission": {"text": "Namespace enforce: restricted rejects new violating Pods: privileged mode, host namespaces/ports, root, missing capability drops or Unconfined seccomp.", "components": ["kubernetes", "kubernetes-134"], "sources": ["kubernetes:s1a2d717974c7", "kubernetes-134:s9b9aaf5067e2"], "status": "REASONED"}, - "existing-pods": {"text": "Adding the namespace label warns about existing violating Pods without evicting them; recreate workloads.", "components": ["kubernetes", "kubernetes-134"], "sources": ["kubernetes:s1a2d717974c7", "kubernetes-134:sa25682f200f0"], "status": "REASONED"}, - "policy-semantics": {"text": "NetworkPolicies are additive and require an enforcing CNI; both source egress and destination ingress must allow a connection.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, - "default-deny": {"text": "The empty podSelector with Ingress and Egress isolates all pods selected in the namespace before narrow allowances.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, - "dns": {"text": "The app egress rule permits UDP/TCP 53 to all kube-system pods, not just CoreDNS; narrow it to the actual resolver.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, - "database-path": {"text": "Allow app egress to role: db and db ingress from role: app on TCP 5432; other paths governed by these namespace policies are refused.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, - "policy-limits": {"text": "hostNetwork and same-node traffic handling are implementation-dependent; database TLS/authentication remain separate controls.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, - "verify-user": {"text": "Compose exec targets the app service in the resolved project and id must report a nonzero UID; using the actual container name for bare docker exec is this guide's advice.", "components": ["docker", "compose-2324"], "sources": ["docker:s6ca35cbd69e7", "docker:s8b7485a246e4", "compose-2324:s906b5822e9c4", "compose-2324:sfe4c568a9887"], "status": "REASONED", "verify": [1]}, - "verify-rootfs": {"text": "With read_only false, touch on an existing owned path must print WRITABLE; after docker compose up -d --force-recreate with read_only true, it must print blocked. ENOENT or EACCES can also print blocked on a writable filesystem, so blocked alone is not proof of a read-only root. This guide's assessment: the errno, including typical EROFS, depends on the image and path; the probe's exit 0 does not discriminate.", "components": ["docker", "compose-2324"], "sources": ["docker:s8b7485a246e4", "compose-2324:s1bf114813e47"], "status": "REASONED", "verify": [1]}, - "verify-context": {"text": "kubectl get reads the declared context; kubectl exec inspects the application process for nonzero UID, NoNewPrivs:1, Seccomp:2 and zero effective/bounding capabilities. Clearing CapBnd under drop:[ALL] is this guide's expected runtime state; with shareProcessNamespace, PID 1 belongs to the pod sandbox, so select the application PID.", "components": ["kubernetes", "kubectl-134", "procfs-619", "kubernetes-website"], "sources": ["kubernetes:sb78ea91d0308", "kubectl-134:sf5417ce59241", "procfs-619:sa94440d59945", "procfs-619:s74035196d5f7", "kubernetes-website:s3df8336a2aa8"], "status": "REASONED", "verify": [1]}, - "verify-probe": {"text": "Resolve the db ClusterIP once; keep matching container names and restricted-compliant overrides for busybox:1.36 probes. Pinned kubectl and BusyBox sources explain merge and nc flags; the busybox:1.36 image's nc build and options remain unverified.", "components": ["kubernetes", "kubectl-134", "rfc7396", "busybox-1361"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7", "kubernetes:s953450b4076f", "kubectl-134:s0906dc78482b", "kubectl-134:s1adb8cde69c0", "kubectl-134:sc98b92ba3ebf", "kubectl-134:s2ffc04de1d63", "rfc7396:sda39f95d0890", "busybox-1361:s14b89daa008d", "busybox-1361:s34bdee292f79", "busybox-1361:s8a53fb647996"], "status": "REASONED", "verify": [1]}, - "verify-segmentation": {"text": "The role: app TCP probe must succeed first; role: other to the same IP must fail at TCP, not DNS. This tests combined segmentation, not either policy layer independently.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED", "verify": [1]} - } -} ---- # Containers: non-root, dropped capabilities, read-only root, and network segmentation - -**Version basis** +Written against Docker documentation (no recorded version), Docker Compose source and reference v2.32.4, Kubernetes Pod Security Admission 1.34, kubectl source v1.34.0, Linux process status and capabilities manuals man-pages-6.19, JSON Merge Patch RFC 7396, and BusyBox source 1_36_1; 2 further sources are pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 22 claims, all reasoned from cited sources. -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-18; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- docker: Docker documentation (unknown) -- docker-daemon: Docker daemon security documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- compose-2324: Docker Compose source and reference (v2.32.4) -- procfs-619: Linux process status and capabilities manuals (man-pages-6.19) -- kubernetes-website: Kubernetes process namespace documentation (47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8) -- kubernetes: Kubernetes documentation (unknown) -- kubernetes-134: Kubernetes Pod Security Admission (1.34) -- kubectl-134: kubectl source (v1.34.0) -- rfc7396: JSON Merge Patch (RFC 7396) -- busybox-1361: BusyBox source (1_36_1) - -Claims (full source mapping in the front matter): - -- docker-user (REASONED; docker): Dockerfile USER sets build/runtime identity; set user and group, since no primary group uses root. Compose user overrides it; unset in both runs as root. -- docker-rootfs (REASONED; docker): Compose read_only makes the root filesystem read-only; provide tmpfs only for required writable paths. -- docker-capabilities (REASONED; docker): cap_drop: [ALL] removes capabilities; add back only specific required capabilities with cap_add. -- docker-escalation (REASONED; docker): no-new-privileges prevents setuid privilege gain; the guide lists bare, equals-true and colon-true spellings as equivalent. -- docker-socket (REASONED; docker, docker-daemon): Never mount /var/run/docker.sock. In Docker's default rootful configuration, daemon control lets a user start a container that can alter the host filesystem without restriction, and docker-group membership grants root-level privileges; Rootless mode runs the daemon and containers without root privileges. Docker documents mounted-socket daemon control with a Docker client, and documents --privileged and host bind mounts; a socket-holding container starting a privileged sibling or bind-mounting host / to escape is this guide's assessment. Rootless mode narrowing the available host privilege but not making the mount safe is this guide's assessment. -- kube-user (REASONED; kubernetes): runAsNonRoot rejects root; pin runAsUser and runAsGroup to 10001, with the guide noting runtime-default GID 0 if unset. -- kube-rootfs (REASONED; kubernetes): Set readOnlyRootFilesystem per container; this is a separate recommendation, not a restricted Pod Security requirement. -- kube-escalation (REASONED; kubernetes): Set allowPrivilegeEscalation: false in the container securityContext. -- kube-capabilities (REASONED; kubernetes): Drop ALL capabilities in the container securityContext. -- kube-seccomp (REASONED; kubernetes): RuntimeDefault selects the runtime syscall filter instead of Unconfined. -- admission (REASONED; kubernetes, kubernetes-134): Namespace enforce: restricted rejects new violating Pods: privileged mode, host namespaces/ports, root, missing capability drops or Unconfined seccomp. -- existing-pods (REASONED; kubernetes, kubernetes-134): Adding the namespace label warns about existing violating Pods without evicting them; recreate workloads. -- policy-semantics (REASONED; kubernetes): NetworkPolicies are additive and require an enforcing CNI; both source egress and destination ingress must allow a connection. -- default-deny (REASONED; kubernetes): The empty podSelector with Ingress and Egress isolates all pods selected in the namespace before narrow allowances. -- dns (REASONED; kubernetes): The app egress rule permits UDP/TCP 53 to all kube-system pods, not just CoreDNS; narrow it to the actual resolver. -- database-path (REASONED; kubernetes): Allow app egress to role: db and db ingress from role: app on TCP 5432; other paths governed by these namespace policies are refused. -- policy-limits (REASONED; kubernetes): hostNetwork and same-node traffic handling are implementation-dependent; database TLS/authentication remain separate controls. -- verify-user (REASONED; docker, compose-2324): Compose exec targets the app service in the resolved project and id must report a nonzero UID; using the actual container name for bare docker exec is this guide's advice. -- verify-rootfs (REASONED; docker, compose-2324): With read_only false, touch on an existing owned path must print WRITABLE; after docker compose up -d --force-recreate with read_only true, it must print blocked. ENOENT or EACCES can also print blocked on a writable filesystem, so blocked alone is not proof of a read-only root. This guide's assessment: the errno, including typical EROFS, depends on the image and path; the probe's exit 0 does not discriminate. -- verify-context (REASONED; kubernetes, kubectl-134, procfs-619, kubernetes-website): kubectl get reads the declared context; kubectl exec inspects the application process for nonzero UID, NoNewPrivs:1, Seccomp:2 and zero effective/bounding capabilities. Clearing CapBnd under drop:[ALL] is this guide's expected runtime state; with shareProcessNamespace, PID 1 belongs to the pod sandbox, so select the application PID. -- verify-probe (REASONED; kubernetes, kubectl-134, rfc7396, busybox-1361): Resolve the db ClusterIP once; keep matching container names and restricted-compliant overrides for busybox:1.36 probes. Pinned kubectl and BusyBox sources explain merge and nc flags; the busybox:1.36 image's nc build and options remain unverified. -- verify-segmentation (REASONED; kubernetes): The role: app TCP probe must succeed first; role: other to the same IP must fail at TCP, not DNS. This tests combined segmentation, not either policy layer independently. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. A model server, chat UI, or proxy is the thing most likely to face untrusted input, so treat its container as compromised eventually and limit what that gets an attacker: not root, not the host's capabilities, not a writable filesystem, and not a route to the rest of the network. @@ -358,3 +193,194 @@ kubectl run probe-forbidden --rm -it --restart=Never --image=busybox:1.36 --labe - Linux man-pages man-pages-6.19 capability bounding set (checked October 2026): https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man/man7/capabilities.7?h=man-pages-6.19#n1367 - kubectl v1.34.0 exec Pod and container examples (checked October 2026): https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/exec/exec.go#L52-L56 - Kubernetes website 47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8 shared process namespace and pod sandbox (checked October 2026): https://github.com/kubernetes/website/blob/47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8/content/en/docs/tasks/configure-pod-container/share-process-namespace.md#L115-L119 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/container-hardening.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- docker: Docker documentation (unknown) +- docker-daemon: Docker daemon security documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- compose-2324: Docker Compose source and reference (v2.32.4) +- kubernetes-website: Kubernetes process namespace documentation (47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8) +- kubernetes: Kubernetes documentation (unknown) +- kubernetes-134: Kubernetes Pod Security Admission (1.34) +- kubectl-134: kubectl source (v1.34.0) +- procfs-619: Linux process status and capabilities manuals (man-pages-6.19) +- rfc7396: JSON Merge Patch (RFC 7396) +- busybox-1361: BusyBox source (1_36_1) + +Claims (full source mapping in the JSON block below): + +- docker-user (REASONED; docker): Dockerfile USER sets build/runtime identity; set user and group, since no primary group uses root. Compose user overrides it; unset in both runs as root. +- docker-rootfs (REASONED; docker): Compose read_only makes the root filesystem read-only; provide tmpfs only for required writable paths. +- docker-capabilities (REASONED; docker): cap_drop: [ALL] removes capabilities; add back only specific required capabilities with cap_add. +- docker-escalation (REASONED; docker): no-new-privileges prevents setuid privilege gain; the guide lists bare, equals-true and colon-true spellings as equivalent. +- docker-socket (REASONED; docker, docker-daemon): Never mount /var/run/docker.sock. In Docker's default rootful configuration, daemon control lets a user start a container that can alter the host filesystem without restriction, and docker-group membership grants root-level privileges; Rootless mode runs the daemon and containers without root privileges. Docker documents mounted-socket daemon control with a Docker client, and documents --privileged and host bind mounts; a socket-holding container starting a privileged sibling or bind-mounting host / to escape is this guide's assessment. Rootless mode narrowing the available host privilege but not making the mount safe is this guide's assessment. +- kube-user (REASONED; kubernetes): runAsNonRoot rejects root; pin runAsUser and runAsGroup to 10001, with the guide noting runtime-default GID 0 if unset. +- kube-rootfs (REASONED; kubernetes): Set readOnlyRootFilesystem per container; this is a separate recommendation, not a restricted Pod Security requirement. +- kube-escalation (REASONED; kubernetes): Set allowPrivilegeEscalation: false in the container securityContext. +- kube-capabilities (REASONED; kubernetes): Drop ALL capabilities in the container securityContext. +- kube-seccomp (REASONED; kubernetes): RuntimeDefault selects the runtime syscall filter instead of Unconfined. +- admission (REASONED; kubernetes, kubernetes-134): Namespace enforce: restricted rejects new violating Pods: privileged mode, host namespaces/ports, root, missing capability drops or Unconfined seccomp. +- existing-pods (REASONED; kubernetes, kubernetes-134): Adding the namespace label warns about existing violating Pods without evicting them; recreate workloads. +- policy-semantics (REASONED; kubernetes): NetworkPolicies are additive and require an enforcing CNI; both source egress and destination ingress must allow a connection. +- default-deny (REASONED; kubernetes): The empty podSelector with Ingress and Egress isolates all pods selected in the namespace before narrow allowances. +- dns (REASONED; kubernetes): The app egress rule permits UDP/TCP 53 to all kube-system pods, not just CoreDNS; narrow it to the actual resolver. +- database-path (REASONED; kubernetes): Allow app egress to role: db and db ingress from role: app on TCP 5432; other paths governed by these namespace policies are refused. +- policy-limits (REASONED; kubernetes): hostNetwork and same-node traffic handling are implementation-dependent; database TLS/authentication remain separate controls. +- verify-user (REASONED; docker, compose-2324): Compose exec targets the app service in the resolved project and id must report a nonzero UID; using the actual container name for bare docker exec is this guide's advice. +- verify-rootfs (REASONED; docker, compose-2324): With read_only false, touch on an existing owned path must print WRITABLE; after docker compose up -d --force-recreate with read_only true, it must print blocked. ENOENT or EACCES can also print blocked on a writable filesystem, so blocked alone is not proof of a read-only root. This guide's assessment: the errno, including typical EROFS, depends on the image and path; the probe's exit 0 does not discriminate. +- verify-context (REASONED; kubernetes, kubectl-134, procfs-619, kubernetes-website): kubectl get reads the declared context; kubectl exec inspects the application process for nonzero UID, NoNewPrivs:1, Seccomp:2 and zero effective/bounding capabilities. Clearing CapBnd under drop:[ALL] is this guide's expected runtime state; with shareProcessNamespace, PID 1 belongs to the pod sandbox, so select the application PID. +- verify-probe (REASONED; kubernetes, kubectl-134, rfc7396, busybox-1361): Resolve the db ClusterIP once; keep matching container names and restricted-compliant overrides for busybox:1.36 probes. Pinned kubectl and BusyBox sources explain merge and nc flags; the busybox:1.36 image's nc build and options remain unverified. +- verify-segmentation (REASONED; kubernetes): The role: app TCP probe must succeed first; role: other to the same IP must fail at TCP, not DNS. This tests combined segmentation, not either policy layer independently. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "096aac84d8e85e741360a95b6e4fbadf6b3621a153725920785f898bb479528b", + "components": { + "docker": { + "name": "Docker documentation", + "basis": "unknown", + "sources": { + "s6ca35cbd69e7": "https://docs.docker.com/reference/dockerfile/", + "s8b7485a246e4": "https://docs.docker.com/reference/compose-file/", + "s5d6294cf605e": "https://docs.docker.com/reference/cli/docker/container/run/" + } + }, + "docker-daemon": { + "name": "Docker daemon security documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "secebea1a69ab": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/_index.md#L84-L112", + "s79490cd2f3b2": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/install/linux-postinstall.md#L29-L39", + "s8eab393a61c6": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/rootless/_index.md#L8-L21", + "s94d57fcbfafe": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1330-L1350", + "s372ddf9da1f0": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1460-L1467", + "see4d326cd914": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1495-L1515" + } + }, + "compose-2324": { + "name": "Docker Compose source and reference", + "basis": "v2.32.4", + "sources": { + "s906b5822e9c4": "https://github.com/docker/compose/blob/v2.32.4/docs/reference/compose_exec.md#L1-L7", + "sfe4c568a9887": "https://github.com/docker/compose/blob/v2.32.4/cmd/compose/exec.go#L84-L110", + "s1bf114813e47": "https://github.com/docker/compose/blob/v2.32.4/cmd/compose/up.go#L145-L155" + } + }, + "kubernetes-website": { + "name": "Kubernetes process namespace documentation", + "basis": "47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8", + "sources": { + "s3df8336a2aa8": "https://github.com/kubernetes/website/blob/47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8/content/en/docs/tasks/configure-pod-container/share-process-namespace.md#L115-L119" + } + }, + "kubernetes": { + "name": "Kubernetes documentation", + "basis": "unknown", + "sources": { + "sb78ea91d0308": "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/", + "s1a2d717974c7": "https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "s953450b4076f": "https://kubernetes.io/docs/concepts/services-networking/network-policies/" + } + }, + "kubernetes-134": { + "name": "Kubernetes Pod Security Admission", + "basis": "1.34", + "sources": { + "s9b9aaf5067e2": "https://v1-34.docs.kubernetes.io/docs/concepts/security/pod-security-admission/", + "sa25682f200f0": "https://v1-34.docs.kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/" + } + }, + "kubectl-134": { + "name": "kubectl source", + "basis": "v1.34.0", + "sources": { + "s0906dc78482b": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L293-L310", + "s1adb8cde69c0": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L103-L194", + "sc98b92ba3ebf": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L591-L622", + "s2ffc04de1d63": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/util/override_options.go#L29-L90", + "sf5417ce59241": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/exec/exec.go#L52-L56" + } + }, + "procfs-619": { + "name": "Linux process status and capabilities manuals", + "basis": "man-pages-6.19", + "sources": { + "sa94440d59945": "https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man/man5/proc_pid_status.5?h=man-pages-6.19#n315", + "s74035196d5f7": "https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man/man7/capabilities.7?h=man-pages-6.19#n1367" + } + }, + "rfc7396": { + "name": "JSON Merge Patch", + "basis": "RFC 7396", + "sources": { + "sda39f95d0890": "https://www.rfc-editor.org/rfc/rfc7396#section-2" + } + }, + "busybox-1361": { + "name": "BusyBox source", + "basis": "1_36_1", + "sources": { + "s14b89daa008d": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc.c#L30-L59", + "s34bdee292f79": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc_bloaty.c#L61-L93", + "s8a53fb647996": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc_bloaty.c#L888-L922" + } + } + }, + "claims": { + "docker-user": {"text": "Dockerfile USER sets build/runtime identity; set user and group, since no primary group uses root. Compose user overrides it; unset in both runs as root.", "components": ["docker"], "sources": ["docker:s6ca35cbd69e7", "docker:s8b7485a246e4"], "status": "REASONED"}, + "docker-rootfs": {"text": "Compose read_only makes the root filesystem read-only; provide tmpfs only for required writable paths.", "components": ["docker"], "sources": ["docker:s8b7485a246e4"], "status": "REASONED"}, + "docker-capabilities": {"text": "cap_drop: [ALL] removes capabilities; add back only specific required capabilities with cap_add.", "components": ["docker"], "sources": ["docker:s8b7485a246e4"], "status": "REASONED"}, + "docker-escalation": {"text": "no-new-privileges prevents setuid privilege gain; the guide lists bare, equals-true and colon-true spellings as equivalent.", "components": ["docker"], "sources": ["docker:s5d6294cf605e", "docker:s8b7485a246e4"], "status": "REASONED"}, + "docker-socket": {"text": "Never mount /var/run/docker.sock. In Docker's default rootful configuration, daemon control lets a user start a container that can alter the host filesystem without restriction, and docker-group membership grants root-level privileges; Rootless mode runs the daemon and containers without root privileges. Docker documents mounted-socket daemon control with a Docker client, and documents --privileged and host bind mounts; a socket-holding container starting a privileged sibling or bind-mounting host / to escape is this guide's assessment. Rootless mode narrowing the available host privilege but not making the mount safe is this guide's assessment.", "components": ["docker", "docker-daemon"], "sources": ["docker:s5d6294cf605e", "docker-daemon:secebea1a69ab", "docker-daemon:s79490cd2f3b2", "docker-daemon:s8eab393a61c6", "docker-daemon:s94d57fcbfafe", "docker-daemon:s372ddf9da1f0", "docker-daemon:see4d326cd914"], "status": "REASONED"}, + "kube-user": {"text": "runAsNonRoot rejects root; pin runAsUser and runAsGroup to 10001, with the guide noting runtime-default GID 0 if unset.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308"], "status": "REASONED"}, + "kube-rootfs": {"text": "Set readOnlyRootFilesystem per container; this is a separate recommendation, not a restricted Pod Security requirement.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, + "kube-escalation": {"text": "Set allowPrivilegeEscalation: false in the container securityContext.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, + "kube-capabilities": {"text": "Drop ALL capabilities in the container securityContext.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, + "kube-seccomp": {"text": "RuntimeDefault selects the runtime syscall filter instead of Unconfined.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, + "admission": {"text": "Namespace enforce: restricted rejects new violating Pods: privileged mode, host namespaces/ports, root, missing capability drops or Unconfined seccomp.", "components": ["kubernetes", "kubernetes-134"], "sources": ["kubernetes:s1a2d717974c7", "kubernetes-134:s9b9aaf5067e2"], "status": "REASONED"}, + "existing-pods": {"text": "Adding the namespace label warns about existing violating Pods without evicting them; recreate workloads.", "components": ["kubernetes", "kubernetes-134"], "sources": ["kubernetes:s1a2d717974c7", "kubernetes-134:sa25682f200f0"], "status": "REASONED"}, + "policy-semantics": {"text": "NetworkPolicies are additive and require an enforcing CNI; both source egress and destination ingress must allow a connection.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "default-deny": {"text": "The empty podSelector with Ingress and Egress isolates all pods selected in the namespace before narrow allowances.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "dns": {"text": "The app egress rule permits UDP/TCP 53 to all kube-system pods, not just CoreDNS; narrow it to the actual resolver.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "database-path": {"text": "Allow app egress to role: db and db ingress from role: app on TCP 5432; other paths governed by these namespace policies are refused.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "policy-limits": {"text": "hostNetwork and same-node traffic handling are implementation-dependent; database TLS/authentication remain separate controls.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "verify-user": {"text": "Compose exec targets the app service in the resolved project and id must report a nonzero UID; using the actual container name for bare docker exec is this guide's advice.", "components": ["docker", "compose-2324"], "sources": ["docker:s6ca35cbd69e7", "docker:s8b7485a246e4", "compose-2324:s906b5822e9c4", "compose-2324:sfe4c568a9887"], "status": "REASONED", "verify": [1]}, + "verify-rootfs": {"text": "With read_only false, touch on an existing owned path must print WRITABLE; after docker compose up -d --force-recreate with read_only true, it must print blocked. ENOENT or EACCES can also print blocked on a writable filesystem, so blocked alone is not proof of a read-only root. This guide's assessment: the errno, including typical EROFS, depends on the image and path; the probe's exit 0 does not discriminate.", "components": ["docker", "compose-2324"], "sources": ["docker:s8b7485a246e4", "compose-2324:s1bf114813e47"], "status": "REASONED", "verify": [1]}, + "verify-context": {"text": "kubectl get reads the declared context; kubectl exec inspects the application process for nonzero UID, NoNewPrivs:1, Seccomp:2 and zero effective/bounding capabilities. Clearing CapBnd under drop:[ALL] is this guide's expected runtime state; with shareProcessNamespace, PID 1 belongs to the pod sandbox, so select the application PID.", "components": ["kubernetes", "kubectl-134", "procfs-619", "kubernetes-website"], "sources": ["kubernetes:sb78ea91d0308", "kubectl-134:sf5417ce59241", "procfs-619:sa94440d59945", "procfs-619:s74035196d5f7", "kubernetes-website:s3df8336a2aa8"], "status": "REASONED", "verify": [1]}, + "verify-probe": {"text": "Resolve the db ClusterIP once; keep matching container names and restricted-compliant overrides for busybox:1.36 probes. Pinned kubectl and BusyBox sources explain merge and nc flags; the busybox:1.36 image's nc build and options remain unverified.", "components": ["kubernetes", "kubectl-134", "rfc7396", "busybox-1361"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7", "kubernetes:s953450b4076f", "kubectl-134:s0906dc78482b", "kubectl-134:s1adb8cde69c0", "kubectl-134:sc98b92ba3ebf", "kubectl-134:s2ffc04de1d63", "rfc7396:sda39f95d0890", "busybox-1361:s14b89daa008d", "busybox-1361:s34bdee292f79", "busybox-1361:s8a53fb647996"], "status": "REASONED", "verify": [1]}, + "verify-segmentation": {"text": "The role: app TCP probe must succeed first; role: other to the same IP must fail at TCP, not DNS. This tests combined segmentation, not either policy layer independently.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/cors.md b/cors.md index b7adb2d92..c99238183 100644 --- a/cors.md +++ b/cors.md @@ -1,88 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-27", - "documentation_checked": "2026-09", - "body_sha256": "1ab4078e43ca8a5f60f429011121b3a68578a9252ac5d688dfbcd65f7b1ad6cd", - "components": { - "protocol": { - "name": "MDN CORS documentation", - "basis": "unknown", - "sources": { - "s6265cf028385": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS" - } - }, - "express": { - "name": "Express cors middleware", - "basis": "unknown", - "sources": { - "sb6448b9a443d": "https://expressjs.com/en/resources/middleware/cors/" - } - }, - "fastapi": { - "name": "FastAPI CORSMiddleware", - "basis": "unknown", - "sources": { - "s4f1cea92e7bc": "https://fastapi.tiangolo.com/tutorial/cors/" - } - }, - "fetch": { - "name": "Fetch Standard credentials", - "basis": "unknown", - "sources": { - "s028c38ebd452": "https://fetch.spec.whatwg.org/#credentials" - } - } - }, - "claims": { - "exact-origins": {"text": "Allow exact browser origins, illustrated by Access-Control-Allow-Origin: https://app.example.com; keep environment-specific lists in configuration instead of production localhost entries.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, - "wildcard-credentials": {"text": "Browsers refuse wildcard Access-Control-Allow-Origin with credentials; the guide restricts wildcard use to public, unauthenticated, read-only resources.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, - "origin-reflection": {"text": "Reflect only origins checked against an explicit allowlist; reflecting arbitrary Origin values permits hostile sites to read credentialed responses.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, - "authentication": {"text": "CORS controls browsers rather than curl or other attackers, does not expose a port and does not replace endpoint authentication.", "components": ["protocol", "express"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d"], "status": "REASONED"}, - "express-origins": {"text": "Express cors uses origin: ['https://app.example.com'] as the allowed origin list.", "components": ["express"], "sources": ["express:sb6448b9a443d"], "status": "REASONED"}, - "express-credentials": {"text": "The Express example enables credentials with credentials: true.", "components": ["express"], "sources": ["express:sb6448b9a443d"], "status": "REASONED"}, - "fastapi-origins": {"text": "FastAPI adds CORSMiddleware with allow_origins restricted to https://app.example.com.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, - "fastapi-credentials": {"text": "The FastAPI example enables allow_credentials=True.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, - "fastapi-methods": {"text": "The FastAPI example allows only GET and POST through allow_methods.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, - "fastapi-headers": {"text": "The FastAPI example allows Authorization and Content-Type through allow_headers.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, - "verify-hostile": {"text": "The exposed API echoes https://evil.example in Access-Control-Allow-Origin; the fixed API omits that hostile origin. No deployed API or browser run is recorded.", "components": ["protocol", "express", "fastapi", "fetch"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d", "fastapi:s4f1cea92e7bc", "fetch:s028c38ebd452"], "status": "REASONED", "verify": [1]}, - "verify-allowed": {"text": "The allowed-Origin probe should return the listed app origin, with Allow-Credentials only where the browser must send cookies, HTTP authentication or TLS client certificates; no actual response is recorded.", "components": ["protocol", "express", "fastapi", "fetch"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d", "fastapi:s4f1cea92e7bc", "fetch:s028c38ebd452"], "status": "REASONED", "verify": [1]}, - "credential-types": {"text": "Browser credentials include cookies, HTTP authentication and TLS client certificates; Access-Control-Allow-Credentials: true is needed only where the browser must send credentials.", "components": ["fetch", "protocol"], "sources": ["fetch:s028c38ebd452", "protocol:s6265cf028385"], "status": "REASONED"} - } -} ---- # CORS: allow your origins, not everyone's - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-09-27; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- protocol: MDN CORS documentation (unknown) -- express: Express cors middleware (unknown) -- fastapi: FastAPI CORSMiddleware (unknown) -- fetch: Fetch Standard credentials (unknown) - -Claims (full source mapping in the front matter): +Written against MDN CORS documentation (no recorded version); 3 further sources carry no recorded version. Metadata reviewed 2026-09-27; documentation checked September 2026. The guide records 13 claims, all reasoned from cited sources. -- exact-origins (REASONED; protocol): Allow exact browser origins, illustrated by Access-Control-Allow-Origin: https://app.example.com; keep environment-specific lists in configuration instead of production localhost entries. -- wildcard-credentials (REASONED; protocol): Browsers refuse wildcard Access-Control-Allow-Origin with credentials; the guide restricts wildcard use to public, unauthenticated, read-only resources. -- origin-reflection (REASONED; protocol): Reflect only origins checked against an explicit allowlist; reflecting arbitrary Origin values permits hostile sites to read credentialed responses. -- authentication (REASONED; protocol, express): CORS controls browsers rather than curl or other attackers, does not expose a port and does not replace endpoint authentication. -- express-origins (REASONED; express): Express cors uses origin: ['https://app.example.com'] as the allowed origin list. -- express-credentials (REASONED; express): The Express example enables credentials with credentials: true. -- fastapi-origins (REASONED; fastapi): FastAPI adds CORSMiddleware with allow_origins restricted to https://app.example.com. -- fastapi-credentials (REASONED; fastapi): The FastAPI example enables allow_credentials=True. -- fastapi-methods (REASONED; fastapi): The FastAPI example allows only GET and POST through allow_methods. -- fastapi-headers (REASONED; fastapi): The FastAPI example allows Authorization and Content-Type through allow_headers. -- verify-hostile (REASONED; protocol, express, fastapi, fetch): The exposed API echoes https://evil.example in Access-Control-Allow-Origin; the fixed API omits that hostile origin. No deployed API or browser run is recorded. -- verify-allowed (REASONED; protocol, express, fastapi, fetch): The allowed-Origin probe should return the listed app origin, with Allow-Credentials only where the browser must send cookies, HTTP authentication or TLS client certificates; no actual response is recorded. -- credential-types (REASONED; fetch, protocol): Browser credentials include cookies, HTTP authentication and TLS client certificates; Access-Control-Allow-Credentials: true is needed only where the browser must send credentials. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. CORS misconfiguration does not expose a port; it lets hostile websites use your users' browsers, cookies included, against your API. AI assistants reach for `Access-Control-Allow-Origin: *` the moment a browser console shows a CORS error; that is the wrong fix for any API that authenticates. @@ -137,3 +57,109 @@ Keep the origin list in configuration per environment rather than hardcoding loc - Fetch Standard, definition of credentials: https://fetch.spec.whatwg.org/#credentials - Express `cors` middleware, for the `origin` and `credentials` options used above: https://expressjs.com/en/resources/middleware/cors/ - FastAPI CORS, for `CORSMiddleware` and its parameters: https://fastapi.tiangolo.com/tutorial/cors/ + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/cors.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- protocol: MDN CORS documentation (unknown) +- fetch: Fetch Standard credentials (unknown) +- express: Express cors middleware (unknown) +- fastapi: FastAPI CORSMiddleware (unknown) + +Claims (full source mapping in the JSON block below): + +- exact-origins (REASONED; protocol): Allow exact browser origins, illustrated by Access-Control-Allow-Origin: https://app.example.com; keep environment-specific lists in configuration instead of production localhost entries. +- wildcard-credentials (REASONED; protocol): Browsers refuse wildcard Access-Control-Allow-Origin with credentials; the guide restricts wildcard use to public, unauthenticated, read-only resources. +- origin-reflection (REASONED; protocol): Reflect only origins checked against an explicit allowlist; reflecting arbitrary Origin values permits hostile sites to read credentialed responses. +- authentication (REASONED; protocol, express): CORS controls browsers rather than curl or other attackers, does not expose a port and does not replace endpoint authentication. +- express-origins (REASONED; express): Express cors uses origin: ['https://app.example.com'] as the allowed origin list. +- express-credentials (REASONED; express): The Express example enables credentials with credentials: true. +- fastapi-origins (REASONED; fastapi): FastAPI adds CORSMiddleware with allow_origins restricted to https://app.example.com. +- fastapi-credentials (REASONED; fastapi): The FastAPI example enables allow_credentials=True. +- fastapi-methods (REASONED; fastapi): The FastAPI example allows only GET and POST through allow_methods. +- fastapi-headers (REASONED; fastapi): The FastAPI example allows Authorization and Content-Type through allow_headers. +- verify-hostile (REASONED; protocol, express, fastapi, fetch): The exposed API echoes https://evil.example in Access-Control-Allow-Origin; the fixed API omits that hostile origin. No deployed API or browser run is recorded. +- verify-allowed (REASONED; protocol, express, fastapi, fetch): The allowed-Origin probe should return the listed app origin, with Allow-Credentials only where the browser must send cookies, HTTP authentication or TLS client certificates; no actual response is recorded. +- credential-types (REASONED; fetch, protocol): Browser credentials include cookies, HTTP authentication and TLS client certificates; Access-Control-Allow-Credentials: true is needed only where the browser must send credentials. + +````json +{ + "schema": 1, + "checked": "2026-09-27", + "documentation_checked": "2026-09", + "body_sha256": "1ab4078e43ca8a5f60f429011121b3a68578a9252ac5d688dfbcd65f7b1ad6cd", + "components": { + "protocol": { + "name": "MDN CORS documentation", + "basis": "unknown", + "sources": { + "s6265cf028385": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS" + } + }, + "fetch": { + "name": "Fetch Standard credentials", + "basis": "unknown", + "sources": { + "s028c38ebd452": "https://fetch.spec.whatwg.org/#credentials" + } + }, + "express": { + "name": "Express cors middleware", + "basis": "unknown", + "sources": { + "sb6448b9a443d": "https://expressjs.com/en/resources/middleware/cors/" + } + }, + "fastapi": { + "name": "FastAPI CORSMiddleware", + "basis": "unknown", + "sources": { + "s4f1cea92e7bc": "https://fastapi.tiangolo.com/tutorial/cors/" + } + } + }, + "claims": { + "exact-origins": {"text": "Allow exact browser origins, illustrated by Access-Control-Allow-Origin: https://app.example.com; keep environment-specific lists in configuration instead of production localhost entries.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, + "wildcard-credentials": {"text": "Browsers refuse wildcard Access-Control-Allow-Origin with credentials; the guide restricts wildcard use to public, unauthenticated, read-only resources.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, + "origin-reflection": {"text": "Reflect only origins checked against an explicit allowlist; reflecting arbitrary Origin values permits hostile sites to read credentialed responses.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, + "authentication": {"text": "CORS controls browsers rather than curl or other attackers, does not expose a port and does not replace endpoint authentication.", "components": ["protocol", "express"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d"], "status": "REASONED"}, + "express-origins": {"text": "Express cors uses origin: ['https://app.example.com'] as the allowed origin list.", "components": ["express"], "sources": ["express:sb6448b9a443d"], "status": "REASONED"}, + "express-credentials": {"text": "The Express example enables credentials with credentials: true.", "components": ["express"], "sources": ["express:sb6448b9a443d"], "status": "REASONED"}, + "fastapi-origins": {"text": "FastAPI adds CORSMiddleware with allow_origins restricted to https://app.example.com.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, + "fastapi-credentials": {"text": "The FastAPI example enables allow_credentials=True.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, + "fastapi-methods": {"text": "The FastAPI example allows only GET and POST through allow_methods.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, + "fastapi-headers": {"text": "The FastAPI example allows Authorization and Content-Type through allow_headers.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, + "verify-hostile": {"text": "The exposed API echoes https://evil.example in Access-Control-Allow-Origin; the fixed API omits that hostile origin. No deployed API or browser run is recorded.", "components": ["protocol", "express", "fastapi", "fetch"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d", "fastapi:s4f1cea92e7bc", "fetch:s028c38ebd452"], "status": "REASONED", "verify": [1]}, + "verify-allowed": {"text": "The allowed-Origin probe should return the listed app origin, with Allow-Credentials only where the browser must send cookies, HTTP authentication or TLS client certificates; no actual response is recorded.", "components": ["protocol", "express", "fastapi", "fetch"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d", "fastapi:s4f1cea92e7bc", "fetch:s028c38ebd452"], "status": "REASONED", "verify": [1]}, + "credential-types": {"text": "Browser credentials include cookies, HTTP authentication and TLS client certificates; Access-Control-Allow-Credentials: true is needed only where the browser must send credentials.", "components": ["fetch", "protocol"], "sources": ["fetch:s028c38ebd452", "protocol:s6265cf028385"], "status": "REASONED"} + } +} +```` + diff --git a/docker.md b/docker.md index d83bed956..aa92a588e 100644 --- a/docker.md +++ b/docker.md @@ -1,10 +1,276 @@ ---- -version_basis: { +# Docker and Compose: exposure, TLS, and authentication + +Written against Docker Engine minimum boundary 28.0, Moby Docker service unit v28.3.3, Docker firewalld reload fix 28.3.3, systemd unit ordering v262, Git ignore rules v2.49.0, PostgreSQL 17, Caddy v2.11.4, and nginx authentication source release-1.26.3, plus 3 more recorded bases; 7 further sources are pinned to a commit and 8 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 26 claims, all reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + +Containers are where accidental exposure happens most. Two Docker behaviours cause it: + +1. `ports: - "3000:3000"` (or `-p 3000:3000`) publishes on every host address, `0.0.0.0` and `[::]`, at the time of writing. +2. On Linux, Docker programs iptables/nftables directly, so published ports are reachable **even when UFW or firewalld says the port is blocked**. A `ufw deny 3000` rule does not protect a published container port. + +## 1. Publish nothing except the TLS proxy + +Bind anything that must be reachable from the host to loopback, and give everything else no `ports:` entry at all; containers on the same Compose network reach each other by service name without published ports. + +```yaml +services: + app: + build: . + # no ports: entry; only the proxy is published + db: + image: postgres:17 + # no ports: entry; the app reaches it at db:5432 on the internal network +``` + +Where a host-published port is genuinely needed for local access: + +```yaml + ports: + - "127.0.0.1:3000:3000" +``` + +That host-IP restriction requires Docker Engine 28.0 or later, with the firewalld-reload caveat below. Before 28.0, under the default bridge configuration, a neighbour on the same layer-2 segment could reach a port mapped to a loopback address, a remote host could reach a container on a published port despite the host-IP binding, and an unpublished container port was reachable by routing directly to the container if the host's forwarding policy allowed it; 28.0 fixed all three. Check the running Engine version (`docker version --format '{{.Server.Version}}'`, not the client's); on an older engine, treat the address in a publish string as a convenience, not a boundary, and restrict the port in the `DOCKER-USER` chain below or upstream of the host (a cloud security group or a network ACL), since a host firewall like UFW or firewalld does not reach a published port (see the top of this guide). + +On Linux, when Docker Engine runs in the host's network namespace with firewalld running, Engine 28.2.x and 28.3.0 through 28.3.2 failed to restore container-address filtering rules after a firewalld reload. A remote host with a route to the bridge network could then reach published ports at container addresses, including loopback-only publications; unpublished ports remained filtered. Where firewalld is used, run Engine 28.3.3 or later, which fixes this regression (CVE-2025-54388, GHSA-x4rx-4gw3-53p4). Rootless Mode and Docker Desktop are unaffected by this regression. + +As this guide's assessment, where a port must be published on a routable interface, because other hosts need it but the whole internet does not, the most reliable restriction is upstream of the host, in a cloud security group or a network ACL: it does not depend on Docker's firewall backend, its userland proxy, or the host's boot order. See [cloud-firewalls.md](cloud-firewalls.md) for sourced AWS security-group, Google Cloud VPC firewall and Azure NSG rules; network-ACL configuration is not directly sourced here. On the host itself, filter it in the `DOCKER-USER` iptables chain (Docker's iptables backend only; its nftables backend has no such chain), whose rules Docker evaluates before its own accept rules. Match only new connections, so the rule does not also drop the replies to connections your own containers open, which arrive on the same interface and would otherwise lose their outbound access: + +```bash +# IPv4, run as root; replace ext_if with your external interface and the subnet with your own +iptables -I DOCKER-USER -i ext_if -m conntrack --ctstate NEW ! -s 192.0.2.0/24 -j DROP +``` + +This runs after destination NAT, so an ordinary destination match sees a container's internal IP and port, not the published host IP or port; it also covers every published port arriving on that interface, so scope it to one service with conntrack's `--ctorigdstport` and `--ctdir ORIGINAL` if you need to. As this guide's reasoning, `iptables` covers IPv4 only. Cover native IPv6 forwarding with an equivalent `ip6tables` `DOCKER-USER` rule, but note a second path: publishing without a host IP on an IPv4-only bridge with `--userland-proxy=true` (the default) also makes the port answer on the host's IPv6 addresses through Docker's userland proxy (it maps IPv6 to the container's IPv4). As this guide's reasoning, that connection terminates on a host process, so it traverses the host `INPUT` chain, not `FORWARD`, and a `DOCKER-USER` rule (which lives in `FORWARD`) never sees it. Close that path by publishing to a specific address (`-p 127.0.0.1:3000:3000` or a chosen IPv4), disabling the userland proxy, or restricting the host's IPv6 `INPUT`. Probe IPv4 and IPv6 separately, each from an allowed and a disallowed source. Confirm with `sudo iptables -nvL DOCKER-USER`, watching its counters, and by probing the port from a host outside the allowed subnet, expecting a timeout, and one inside it, expecting a connection. As this guide's assessment, the rule does not survive a reboot and is not in place while the host boots; persist just the DOCKER-USER rules with a startup unit ordered after the Docker service, not a blanket `iptables-persistent` save (which, as this guide's assessment, also captures Docker's own dynamic chains and can break container networking on the next boot), and where the boot-time window matters, rely on the upstream control. + +## 2. Terminate TLS in one proxy container + +Caddy is the least configuration ([caddy.md](caddy.md)); nginx ([nginx.md](nginx.md)) and Traefik ([traefik.md](traefik.md)) work the same way. A complete pattern: + +```yaml +services: + app: + build: . + + caddy: + image: caddy:2 + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + +volumes: + caddy_data: + caddy_config: +``` + +`Caddyfile`: + +```caddyfile +app.example.com { + reverse_proxy app:3000 +} +``` + +Caddy obtains and renews the certificate automatically ([free-certificates.md](free-certificates.md) explains the ACME requirements). As this guide's recommendation, hosts with no inbound ports but a domain you can put on Cloudflare should use [cloudflare.md](cloudflare.md); run the `cloudflared` connector as a container on the app's Compose network and point it at `http://app:3000`. As this guide's recommendation, hosts with no domain at all should use [tailscale.md](tailscale.md) (Serve for private tailnet access or Funnel for public access), or [self-signed.md](self-signed.md) for internal use. Docker documents service-name lookup on the shared Compose network; the `http://app:3000` origin is this guide's inference from that lookup and [cloudflare.md](cloudflare.md)'s HTTP origin example, while the fetched Cloudflare origin parameters show an HTTPS service URL rather than this literal HTTP value. This guide does not show the tunnel-token setup; [cloudflare.md](cloudflare.md) gives the protected `--token-file` path. + +## 3. Authentication and secrets + +- As this guide's assessment, the proxy is the natural place for a first authentication gate (basic auth per the proxy guides, or Cloudflare Access); the application still needs its own login for anything multi-user ([authentication.md](authentication.md)). The proxy is also where MFA attaches for human-facing services ([mfa.md](mfa.md)): Cloudflare Access policies can require a second factor, and an Authelia portal can handle authentication behind Caddy `forward_auth`, nginx `auth_request` or Traefik `forwardAuth`. Require MFA in the governing policy or portal; as this guide's assessment, basic auth alone is single-factor. +- Pass secrets at runtime through environment files or Docker/Compose secrets. Never bake secrets into the image. Never put secrets in Dockerfile `ENV` or `ARG`: Docker documents that both are exposed in the final image; `ENV` also persists in containers from the image, and build arguments may persist in image metadata, provenance attestations and image history. As this guide's assessment, baking a key into an image ships it to every registry the image touches. `docker image history` shows the image's history, including creation commands; as this guide's assessment, the cited reference does not establish that every argument value is shown. For build-time secrets, use a temporary secret mount for the build instruction and do not copy or print the secret into the image. +- Keep `.env` in `.gitignore` (Git ignores matching untracked files, while already tracked files are unaffected), and run containers as a non-root user (`USER` in the Dockerfile); as this guide's assessment, this means a compromised app is not root in the container. As this guide's advice, non-root is the start, not the whole of workload hardening: dropping capabilities (`cap_drop`), `--security-opt no-new-privileges` (or Compose `security_opt`), a read-only root filesystem (Compose `read_only`), and never mounting the Docker socket are in [container-hardening.md](container-hardening.md). Docker documents these option names, that only trusted users should control the daemon, and that Unix permissions can limit access to its control socket; the linked guide's hardening consequences, including the mounted-socket path, remain this guide's assessment. +- Databases in containers still need their own TLS and authentication when anything outside the Compose network connects: see [postgresql.md](postgresql.md), [mysql.md](mysql.md), [mongodb.md](mongodb.md), and [redis.md](redis.md). + +## 4. Verify + +REASONED: publication, redirect, authentication and external isolation checks; no exposed/fixed run is recorded in this guide. Expectations follow the cited Docker networking sources and linked proxy guides; this read-only review has no authorized deployment or external probe hosts. For the unauthenticated API probe, expect 401 from Caddy or nginx basic auth; Caddy `forward_auth` admits auth-service 2xx, but its rejection status is not established by the fetched lines. Expect 401/403 from nginx `auth_request`; the cited Authelia nginx configuration converts its 401 to a 302 login redirect. Traefik `forwardAuth` passes through its auth service's non-2xx status, including a login redirect; Traefik `basicAuth` calls `RequireAuth`, whose response the fetched lines do not show, so 401 is this guide's expectation rather than a status established by the fetched middleware. For Cloudflare Access, this guide expects a 302 login redirect in the configuration described in [cloudflare.md](cloudflare.md); the fetched Cloudflare lines do not establish that status. A 3xx passes only when its printed location is the gate's own login portal (for Cloudflare Access, your team's `*.cloudflareaccess.com`), never the application's own login. The command fails on any 2xx; any status other than the gate's expected rejection or a 3xx to its portal is not a pass; and an authorized request must succeed separately. + +```bash +docker compose ps # project-scoped: only the proxy shows a published (0.0.0.0/[::] or a host IP) binding; review every container on the host, IPv6 included +ss -tulnp # host sockets (TCP and UDP); socket visibility varies by Docker version and config, so a socket listing alone does not prove reachability +curl -q -sI http://app.example.com/ # expect a redirect to https:// with Caddy automatic HTTPS, or after configuring nginx or Traefik to redirect +( set -o pipefail; curl -q -g -sS --noproxy '*' -o /dev/null -w 'http=%{http_code} loc=%{redirect_url}\n' https://app.example.com/api | awk '{ print; if ($1 ~ /^http=2/) { print "FAIL: unauthenticated /api returned " substr($1, 6); exit 1 } }' ) # unauthenticated: inspect the gate's rejection and portal location; then confirm an authorized request succeeds +``` + +REASONED: preceding block; publication and socket inventory, HTTPS redirect, unauthenticated API rejection and authorized API success follow the cited Docker networking sources and linked proxy guides, as do the external isolation probes this paragraph describes; no Docker runtime or external probe hosts are available, and no exposed/fixed result is recorded. Test from a second machine on a different network where possible; the UFW bypass means testing the firewall from the host itself proves nothing about published ports. The `curl` checks above exercise the proxy's redirect and auth, not the backend, so also probe each restricted host address and published port directly (for example port 3000) from an allowed and a disallowed source, IPv4 and IPv6 separately: a loopback-bound or firewalled backend must show unreachable while the proxy stays reachable. + +REASONED: this firewalld reload check follows the Impact section of GHSA-x4rx-4gw3-53p4 and the pinned v28.3.3 rule-restoration source. It has not been demonstrated here because the authoring environment has no Docker runtime, firewalld or external test host. For a loopback-published HTTP backend using default NAT bridge filtering, confirm that the backend answers locally. From a second host with a route through the Docker host to the bridge subnet, run `curl -q -g --noproxy '*' --connect-timeout 3 --max-time 5 -v http://172.17.0.2:3000/`, replacing the address and port with the actual container address and container port. Repeat with a fresh connection after running `sudo firewall-cmd --reload` on the Docker host. Without another control masking the regression, an affected host changes from blocked to connectable; a fixed host remains blocked. Any successful TCP connection, including an HTTP 401 or 403 response, demonstrates reachability. A failed probe alone does not establish that the Engine is patched: confirm the route, target and backend health. Repeat for configured IPv6 addresses and the restricted host-address publications, and confirm that the TLS proxy remains reachable. + +## Sources (checked September 2026) + +- Docker build variables (`ENV` persists in containers; build arguments may persist in metadata, provenance and history), at 4e9a5751518ed8223a8dcde53693badddd72604f: https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/build/building/variables.md#L17-L52 +- Docker build secrets (temporary mounts for build instructions), at 4e9a5751518ed8223a8dcde53693badddd72604f: https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/build/building/secrets.md#L13-L22 +- Docker image history command (image history and creation-command output), at 4e9a5751518ed8223a8dcde53693badddd72604f: https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_image_history.yaml#L1-L118 +- Git v2.49.0 ignore rules for untracked and tracked files (checked October 2026): https://github.com/git/git/blob/v2.49.0/Documentation/gitignore.adoc#L15-L18 +- Docker Dockerfile `USER` instruction, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/_vendor/github.com/moby/buildkit/frontend/dockerfile/docs/reference.md#L2448-L2463 +- Docker Compose `cap_drop` option, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L224-L233 +- Docker Compose `read_only` option, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L1892-L1894 +- Docker Compose `security_opt` and `no-new-privileges` spellings, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L2008-L2021 +- Docker run `--security-opt no-new-privileges`, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L2495-L2502 +- Docker daemon control-socket access and host filesystem control, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/_index.md#L84-L112 +- Docker iptables and ip6tables daemon options, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/firewall-iptables.md#L8-L15 +- Docker iptables DOCKER-USER and FORWARD chains, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/firewall-iptables.md#L17-L42 +- Docker IPv6-to-IPv4 userland-proxy mapping, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L60-L62 +- Docker port publishing with an explicit host address, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L33-L38 +- Docker firewall rules and warning against modifying Docker-created rules, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15 +- Moby Docker service unit, at v28.3.3 (checked October 2026): https://github.com/moby/moby/blob/v28.3.3/contrib/init/systemd/docker.service#L1-L9 +- systemd After= unit ordering, at v262 (checked October 2026): https://github.com/systemd/systemd/blob/v262/man/systemd.unit.xml#L826-L838 +- Docker packet filtering and firewalls: https://docs.docker.com/engine/network/packet-filtering-firewalls/ +- Docker with iptables, for the `DOCKER-USER` chain (processed before Docker's own rules; matches container addresses after DNAT): https://docs.docker.com/engine/network/firewall-iptables/ +- Docker Engine 28.0 release notes, for the published-port and loopback-mapping hardening: https://docs.docker.com/engine/release-notes/28/ +- Docker Engine 28.3.3 security fix: https://docs.docker.com/engine/release-notes/28/#2833 and affected-version advisory (OSV copy of the July 2025 Moby advisory): https://osv.dev/vulnerability/GHSA-x4rx-4gw3-53p4 +- Moby 28.3.3 (pinned tag v28.3.3), `reapplyPerPortIptables` restores endpoint rules after firewalld reload: https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go +- Compose networking: https://docs.docker.com/compose/how-tos/networking/ +- Docker port publishing (with no host address, "the Docker daemon publishes ports to all host addresses (0.0.0.0 and [::])"): https://docs.docker.com/engine/network/port-publishing/ +- PostgreSQL 17 connection settings and default TCP port (checked October 2026): https://www.postgresql.org/docs/17/runtime-config-connection.html +- Caddy image fba2853501d36e8a72f946ac8cb7ff64d07e48f2 data/config paths, exposed ports and Caddyfile command (checked October 2026): https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L44-L45, https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L56-L59, https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L63 +- Caddy v2.11.4 automatic certificate management and reverse_proxy syntax (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L62-L67, https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L60 +- Caddy v2.11.4 authentication handler and 401 rejection (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/caddyauth/caddyauth.go#L84-L120 +- Caddy v2.11.4 basic-auth missing-credential path (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/caddyauth/basicauth.go#L142-L163 +- Caddy v2.11.4 forward_auth accepted-response handler (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go#L174-L194 +- Caddy v2.11.4 automatic redirect setup (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L234-L240 +- Caddy v2.11.4 HTTPS redirect route (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L544-L575 +- nginx release-1.26.3 missing basic-auth credentials (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_basic_module.c#L122-L129 +- nginx release-1.26.3 basic-auth 401 challenge (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_basic_module.c#L317-L348 +- nginx release-1.26.3 auth_request status handling (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_request_module.c#L135-L182 +- Traefik v3.5.0 basicAuth failure path (checked October 2026): https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/basic_auth.go#L67-L99 +- Traefik v3.5.0 forwardAuth keeps the auth service redirect (checked October 2026): https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/forward.go#L93-L99 +- Traefik v3.5.0 forwardAuth rejection status passthrough (checked October 2026): https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/forward.go#L233-L262 +- Cloudflare Access MFA policy enforcement (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L17-L85 +- Cloudflare Access self-hosted application protection (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54 +- Authelia v4.39.28 supported proxy integration (checked October 2026): https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L24-L32 +- Authelia v4.39.28 nginx portal redirect (checked October 2026): https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L537-L541 +- AWS security-group rules (rolling reference reused from `cloud-firewalls.md`; not rechecked here): https://docs.aws.amazon.com/vpc/latest/userguide/security-group-rules.html +- Google Cloud VPC firewall rules (rolling reference reused from `cloud-firewalls.md`; not rechecked here): https://docs.cloud.google.com/firewall/docs/using-firewalls +- Azure NSG rule properties (rolling reference reused from `cloud-firewalls.md`; not rechecked here): https://learn.microsoft.com/en-us/rest/api/virtualnetwork/network-security-groups/get +- cloudflared 2026.9.3 tunnel daemon without firewall openings (checked October 2026): https://github.com/cloudflare/cloudflared/blob/2026.9.3/README.md#L1-L5 +- cloudflared 2026.9.3 Docker image (checked October 2026): https://github.com/cloudflare/cloudflared/blob/2026.9.3/README.md#L26-L33 +- cloudflared 2026.9.3 container entry point (checked October 2026): https://github.com/cloudflare/cloudflared/blob/2026.9.3/Dockerfile#L40-L42 +- Cloudflare Tunnel origin service URL example at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/partials/cloudflare-one/tunnel/origin-parameters.mdx#L31-L49 +- Cloudflare Tunnel published application domain prerequisite at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel.mdx#L27-L37 +- Docker Compose service-name lookup at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/compose/how-tos/networking.md#L13-L17 +- Tailscale Serve private tailnet entry (rolling reference reused from `tailscale.md`; not rechecked here): https://tailscale.com/kb/1242/tailscale-serve +- Tailscale Funnel public entry (rolling reference reused from `tailscale.md`; not rechecked here): https://tailscale.com/kb/1223/funnel +- OpenSSL documentation index (rolling reference reused from `self-signed.md`; not rechecked here): https://docs.openssl.org/ + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/docker.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- boundary: Docker Engine minimum boundary (28.0) +- moby-unit: Moby Docker service unit (v28.3.3) +- reload: Docker firewalld reload fix (28.3.3) +- docker-build: Docker build and image history documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- engine: Docker Engine networking (unknown) +- engine-pinned: Docker Engine networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- compose: Compose networking (unknown) +- docker-workload: Docker workload documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- reload-advisory: Docker firewalld reload advisory (unknown) +- compose-pinned: Docker Compose networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- systemd-unit: systemd unit ordering (v262) +- git-ignore: Git ignore rules (v2.49.0) +- postgresql: PostgreSQL (17) +- caddy-image: Caddy image (fba2853501d36e8a72f946ac8cb7ff64d07e48f2) +- caddy: Caddy (v2.11.4) +- nginx-auth: nginx authentication source (release-1.26.3) +- traefik-auth: Traefik authentication source (v3.5.0) +- cloudflare-access: Cloudflare Access documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- authelia-proxy: Authelia proxy integration (v4.39.28) +- aws-firewall: AWS security group rules (unknown) +- gcp-firewall: Google Cloud VPC firewall rules (unknown) +- azure-firewall: Azure network security group rules (unknown) +- cloudflared-image: cloudflared container image (2026.9.3) +- cloudflare-tunnel: Cloudflare Tunnel documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- tailscale-entry: Tailscale entry documentation (unknown) +- self-signed-entry: OpenSSL documentation (rolling) (unknown) + +Claims (full source mapping in the JSON block below): + +- publish (REASONED; engine): Omitting the host address publishes 3000:3000 on 0.0.0.0 and [::]. +- host-firewall (REASONED; engine): Docker programs firewall rules directly; UFW or firewalld blocking a host port does not establish published-port isolation. +- private-network (REASONED; compose, postgresql): Omit app/database ports; Compose peers use service names. The example uses postgres:17 at db:5432. +- loopback (REASONED; engine, boundary, reload, reload-advisory): Publish local access as 127.0.0.1:3000:3000; check the running server version. Engine 28.0 is the minimum boundary, with 28.3.3 or later required where firewalld is used. +- old-loopback (REASONED; boundary): Before 28.0, same-L2 neighbours could reach loopback publications under the default bridge configuration. +- old-host-bind (REASONED; boundary): Before 28.0, remote hosts could reach published container ports despite the host-IP binding. +- old-unpublished (REASONED; boundary): Before 28.0, direct routing could reach unpublished container ports; 28.0 fixed the stated default-bridge exposures. +- upstream (REASONED; engine, aws-firewall, gcp-firewall, azure-firewall): Use an upstream security group or network ACL for routable publications and boot-time protection. AWS security groups, Google Cloud VPC firewall rules and Azure NSGs have rolling sources; network-ACL configuration and boot-time independence remain this guide's assessment. +- docker-user (REASONED; engine): On the iptables backend, DOCKER-USER precedes Docker accepts; the nftables backend has no such chain. Match NEW traffic from outside the allowed subnet. +- dnat (REASONED; engine): DOCKER-USER sees post-DNAT container destinations; conntrack original destination port and ORIGINAL direction scope a published service. +- ipv6 (REASONED; engine, engine-pinned): As this guide's reasoning, native IPv6 forwarding needs ip6tables; Docker documents IPv6-to-IPv4 userland-proxy mapping when no host IP is given, the bridge is IPv4-only and `--userland-proxy=true` (the default), and places DOCKER-USER in FORWARD. As this guide's reasoning, the proxy path terminates on host INPUT and bypasses DOCKER-USER. +- proxy-path (REASONED; engine, engine-pinned): Close the userland-proxy path with an explicit publish address, disabled userland proxy or IPv6 INPUT restrictions; test both address families and sources. Docker documents the explicit-address option and conditional userland-proxy mapping; the INPUT restriction and probes are this guide's reasoning and advice. +- persistence (REASONED; engine, engine-pinned, moby-unit, systemd-unit): As this guide's advice, persist only DOCKER-USER rules after Docker startup, avoiding blanket dynamic-chain saves. Moby identifies docker.service and systemd documents After= ordering; as this guide's assessment, rule non-persistence, the boot-time gap, what iptables-persistent captures and its potential to break container networking have no direct source here. +- tls (REASONED; compose, caddy-image, caddy): Example caddy:2 publishes 80:80 and 443:443, mounts Caddyfile read-only and persists data/config, proxying app:3000 with automatic certificates. +- alternate-entry (REASONED; compose, cloudflared-image, cloudflare-tunnel, compose-pinned, tailscale-entry, self-signed-entry): As this guide's advice, hosts with no inbound ports and a domain they can put on Cloudflare use a cloudflared container, while hosts with no domain at all use Tailscale or internal self-signed TLS. Cloudflare documents a tunnel daemon that needs no firewall openings, a container image, an HTTPS origin service URL and a domain prerequisite; Docker documents Compose service-name lookup. The http://app:3000 origin target on a shared Compose network is this guide's inference, not shown literally by the fetched Cloudflare lines. The tunnel-token setup is not shown here; the Tailscale and OpenSSL references are rolling, and the self-signed fallback remains this guide's advice. +- auth (REASONED; compose, caddy, nginx-auth, traefik-auth, cloudflare-access, authelia-proxy): As this guide's assessment, proxy authentication and human MFA supplement application login. Caddy and nginx basic authentication reject missing credentials; nginx auth_request returns 401/403; Traefik basicAuth calls RequireAuth and forwardAuth passes through non-2xx; Cloudflare Access policies can require MFA, and Authelia can sit behind the proxy. Require MFA in the governing policy or portal; as this guide's assessment, basic auth alone is single-factor. The Caddy forward_auth rejection status, exact Traefik basicAuth status and Cloudflare Access anonymous status are not established by these fetched lines. +- secrets (REASONED; compose, docker-build): Use runtime environment files or Compose secrets, exclude .env from Git, and never put secrets in Dockerfile ENV/ARG; use temporary build secret mounts. Docker build and history behaviour is sourced; as this guide's assessment, the cited history reference does not establish that every argument value is shown; as this guide's assessment, never bake secrets into images and do not copy or print build-time secrets into the image. No direct source for the runtime delivery methods or the .env exclusion from Git is listed. +- workload (REASONED; compose, git-ignore, docker-workload): Keep .env in .gitignore, which ignores matching untracked files but not tracked files. Use a non-root Dockerfile USER; as this guide's assessment, a compromised app then is not root in the container. Drop capabilities, use no-new-privileges and a read-only root filesystem, and never mount the Docker socket. Docker sources establish the option names, that only trusted users should control the daemon, and that Unix permissions can limit access to its control socket; the linked guide's hardening consequences, including the mounted-socket path, remain this guide's assessment. +- database (REASONED; compose): Database TLS and authentication remain necessary for connections from outside the Compose network; database vendor sources are in linked guides. +- verify-publish (REASONED; compose, engine): Compose ps is project-scoped; inventory every host container and TCP/UDP socket, including IPv6. Socket visibility alone does not prove reachability. +- verify-redirect (REASONED; compose, caddy): Verify that HTTP redirects to HTTPS: Caddy v2.11.4 configures an automatic HTTPS redirect; as this guide's configuration-dependent expectation, nginx and Traefik require a configured redirect. This is an expectation, not an observed deployment result. +- verify-auth (REASONED; compose, caddy, nginx-auth, traefik-auth, authelia-proxy, cloudflare-access): Verify unauthenticated HTTPS /api rejection with printed status and redirect location; the probe itself fails on any 2xx. A 3xx passes only when the location is the gate's own login portal, never the application's own login; verify authorized success separately. Caddy basic auth and nginx auth_basic expect 401; Caddy forward_auth admits auth-service 2xx, but its rejection status is not established by the fetched lines. nginx auth_request returns 401 or 403, while the cited Authelia nginx configuration turns 401 into 302 to its portal. Traefik forwardAuth passes through a rejecting auth service's status, including a login redirect; Traefik basicAuth's 401 is this guide's expectation because the fetched code calls RequireAuth, whose response these lines do not show. Cloudflare Access's anonymous 302 login redirect is this guide's assessment per cloudflare.md, not established by the fetched Cloudflare lines. +- verify-isolation (REASONED; engine): Probe each restricted host address and publication from allowed/disallowed sources in both families; backend isolation must coexist with proxy reachability. +- firewalld-reload (REASONED; reload, reload-advisory): Engine 28.2.x and 28.3.0 through 28.3.2 fail to restore container-address filtering after firewalld reload; 28.3.3 fixes CVE-2025-54388 (GHSA-x4rx-4gw3-53p4). +- firewalld-scope (REASONED; reload, reload-advisory): The regression affects Linux Engine in the host network namespace with firewalld: routed remote hosts can reach published container ports, including loopback publications; unpublished ports stay filtered. Rootless Mode and Docker Desktop are unaffected. +- verify-firewalld-reload (REASONED; reload, reload-advisory): Confirm local backend health, then curl the container address from a second host with a bridge-subnet route before and after sudo firewall-cmd --reload. Without masking controls, affected hosts change from blocked to connectable; fixed hosts stay blocked. Any TCP connection, including HTTP 401/403, shows reachability; failure alone does not prove patching. Check route, target, IPv6, restricted host publications and continued proxy reachability. + +````json +{ "schema": 1, "checked": "2026-10-18", "documentation_checked": "2026-09", "body_sha256": "0a7bc9ec3709af315d7dab37b1fe1b6343f4b7b6425ccde1b041955813382cf5", "components": { + "boundary": { + "name": "Docker Engine minimum boundary", + "basis": "28.0", + "sources": { + "s50eb099eac95": "https://docs.docker.com/engine/release-notes/28/" + } + }, + "moby-unit": { + "name": "Moby Docker service unit", + "basis": "v28.3.3", + "sources": { + "s6aa0d2fb34cf": "https://github.com/moby/moby/blob/v28.3.3/contrib/init/systemd/docker.service#L1-L9" + } + }, + "reload": { + "name": "Docker firewalld reload fix", + "basis": "28.3.3", + "sources": { + "s567a65842b29": "https://docs.docker.com/engine/release-notes/28/#2833", + "seaf4197aafaa": "https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go" + } + }, "docker-build": { "name": "Docker build and image history documentation", "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", @@ -34,39 +300,11 @@ version_basis: { "sc244607b20e4": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15" } }, - "moby-unit": { - "name": "Moby Docker service unit", - "basis": "v28.3.3", - "sources": { - "s6aa0d2fb34cf": "https://github.com/moby/moby/blob/v28.3.3/contrib/init/systemd/docker.service#L1-L9" - } - }, - "systemd-unit": { - "name": "systemd unit ordering", - "basis": "v262", - "sources": { - "s71dce3a476cd": "https://github.com/systemd/systemd/blob/v262/man/systemd.unit.xml#L826-L838" - } - }, - "boundary": { - "name": "Docker Engine minimum boundary", - "basis": "28.0", - "sources": { - "s50eb099eac95": "https://docs.docker.com/engine/release-notes/28/" - } - }, - "compose": { - "name": "Compose networking", - "basis": "unknown", - "sources": { - "sae565a19136c": "https://docs.docker.com/compose/how-tos/networking/" - } - }, - "git-ignore": { - "name": "Git ignore rules", - "basis": "v2.49.0", + "compose": { + "name": "Compose networking", + "basis": "unknown", "sources": { - "s5f377f0fdbd8": "https://github.com/git/git/blob/v2.49.0/Documentation/gitignore.adoc#L15-L18" + "sae565a19136c": "https://docs.docker.com/compose/how-tos/networking/" } }, "docker-workload": { @@ -81,14 +319,6 @@ version_basis: { "secebea1a69ab": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/_index.md#L84-L112" } }, - "reload": { - "name": "Docker firewalld reload fix", - "basis": "28.3.3", - "sources": { - "s567a65842b29": "https://docs.docker.com/engine/release-notes/28/#2833", - "seaf4197aafaa": "https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go" - } - }, "reload-advisory": { "name": "Docker firewalld reload advisory", "basis": "unknown", @@ -96,6 +326,27 @@ version_basis: { "s424f3c4f8285": "https://osv.dev/vulnerability/GHSA-x4rx-4gw3-53p4" } }, + "compose-pinned": { + "name": "Docker Compose networking documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "seb7ab83ee20e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/compose/how-tos/networking.md#L13-L17" + } + }, + "systemd-unit": { + "name": "systemd unit ordering", + "basis": "v262", + "sources": { + "s71dce3a476cd": "https://github.com/systemd/systemd/blob/v262/man/systemd.unit.xml#L826-L838" + } + }, + "git-ignore": { + "name": "Git ignore rules", + "basis": "v2.49.0", + "sources": { + "s5f377f0fdbd8": "https://github.com/git/git/blob/v2.49.0/Documentation/gitignore.adoc#L15-L18" + } + }, "postgresql": { "name": "PostgreSQL", "basis": "17", @@ -197,13 +448,6 @@ version_basis: { "sbd8e87e6c172": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel.mdx#L27-L37" } }, - "compose-pinned": { - "name": "Docker Compose networking documentation", - "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", - "sources": { - "seb7ab83ee20e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/compose/how-tos/networking.md#L13-L17" - } - }, "tailscale-entry": { "name": "Tailscale entry documentation", "basis": "unknown", @@ -249,223 +493,5 @@ version_basis: { "verify-firewalld-reload": {"text": "Confirm local backend health, then curl the container address from a second host with a bridge-subnet route before and after sudo firewall-cmd --reload. Without masking controls, affected hosts change from blocked to connectable; fixed hosts stay blocked. Any TCP connection, including HTTP 401/403, shows reachability; failure alone does not prove patching. Check route, target, IPv6, restricted host publications and continued proxy reachability.", "components": ["reload", "reload-advisory"], "sources": ["reload-advisory:s424f3c4f8285", "reload:seaf4197aafaa"], "status": "REASONED"} } } ---- -# Docker and Compose: exposure, TLS, and authentication - - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-18; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- docker-build: Docker build and image history documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- engine: Docker Engine networking (unknown) -- engine-pinned: Docker Engine networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- moby-unit: Moby Docker service unit (v28.3.3) -- systemd-unit: systemd unit ordering (v262) -- boundary: Docker Engine minimum boundary (28.0) -- compose: Compose networking (unknown) -- git-ignore: Git ignore rules (v2.49.0) -- docker-workload: Docker workload documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- reload: Docker firewalld reload fix (28.3.3) -- reload-advisory: Docker firewalld reload advisory (unknown) -- postgresql: PostgreSQL (17) -- caddy-image: Caddy image (fba2853501d36e8a72f946ac8cb7ff64d07e48f2) -- caddy: Caddy (v2.11.4) -- nginx-auth: nginx authentication source (release-1.26.3) -- traefik-auth: Traefik authentication source (v3.5.0) -- cloudflare-access: Cloudflare Access documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- authelia-proxy: Authelia proxy integration (v4.39.28) -- aws-firewall: AWS security group rules (unknown) -- gcp-firewall: Google Cloud VPC firewall rules (unknown) -- azure-firewall: Azure network security group rules (unknown) -- cloudflared-image: cloudflared container image (2026.9.3) -- cloudflare-tunnel: Cloudflare Tunnel documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- compose-pinned: Docker Compose networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- tailscale-entry: Tailscale entry documentation (unknown) -- self-signed-entry: OpenSSL documentation (rolling) (unknown) - -Claims (full source mapping in the front matter): - -- publish (REASONED; engine): Omitting the host address publishes 3000:3000 on 0.0.0.0 and [::]. -- host-firewall (REASONED; engine): Docker programs firewall rules directly; UFW or firewalld blocking a host port does not establish published-port isolation. -- private-network (REASONED; compose, postgresql): Omit app/database ports; Compose peers use service names. The example uses postgres:17 at db:5432. -- loopback (REASONED; engine, boundary, reload, reload-advisory): Publish local access as 127.0.0.1:3000:3000; check the running server version. Engine 28.0 is the minimum boundary, with 28.3.3 or later required where firewalld is used. -- old-loopback (REASONED; boundary): Before 28.0, same-L2 neighbours could reach loopback publications under the default bridge configuration. -- old-host-bind (REASONED; boundary): Before 28.0, remote hosts could reach published container ports despite the host-IP binding. -- old-unpublished (REASONED; boundary): Before 28.0, direct routing could reach unpublished container ports; 28.0 fixed the stated default-bridge exposures. -- upstream (REASONED; engine, aws-firewall, gcp-firewall, azure-firewall): Use an upstream security group or network ACL for routable publications and boot-time protection. AWS security groups, Google Cloud VPC firewall rules and Azure NSGs have rolling sources; network-ACL configuration and boot-time independence remain this guide's assessment. -- docker-user (REASONED; engine): On the iptables backend, DOCKER-USER precedes Docker accepts; the nftables backend has no such chain. Match NEW traffic from outside the allowed subnet. -- dnat (REASONED; engine): DOCKER-USER sees post-DNAT container destinations; conntrack original destination port and ORIGINAL direction scope a published service. -- ipv6 (REASONED; engine, engine-pinned): As this guide's reasoning, native IPv6 forwarding needs ip6tables; Docker documents IPv6-to-IPv4 userland-proxy mapping when no host IP is given, the bridge is IPv4-only and `--userland-proxy=true` (the default), and places DOCKER-USER in FORWARD. As this guide's reasoning, the proxy path terminates on host INPUT and bypasses DOCKER-USER. -- proxy-path (REASONED; engine, engine-pinned): Close the userland-proxy path with an explicit publish address, disabled userland proxy or IPv6 INPUT restrictions; test both address families and sources. Docker documents the explicit-address option and conditional userland-proxy mapping; the INPUT restriction and probes are this guide's reasoning and advice. -- persistence (REASONED; engine, engine-pinned, moby-unit, systemd-unit): As this guide's advice, persist only DOCKER-USER rules after Docker startup, avoiding blanket dynamic-chain saves. Moby identifies docker.service and systemd documents After= ordering; as this guide's assessment, rule non-persistence, the boot-time gap, what iptables-persistent captures and its potential to break container networking have no direct source here. -- tls (REASONED; compose, caddy-image, caddy): Example caddy:2 publishes 80:80 and 443:443, mounts Caddyfile read-only and persists data/config, proxying app:3000 with automatic certificates. -- alternate-entry (REASONED; compose, cloudflared-image, cloudflare-tunnel, compose-pinned, tailscale-entry, self-signed-entry): As this guide's advice, hosts with no inbound ports and a domain they can put on Cloudflare use a cloudflared container, while hosts with no domain at all use Tailscale or internal self-signed TLS. Cloudflare documents a tunnel daemon that needs no firewall openings, a container image, an HTTPS origin service URL and a domain prerequisite; Docker documents Compose service-name lookup. The http://app:3000 origin target on a shared Compose network is this guide's inference, not shown literally by the fetched Cloudflare lines. The tunnel-token setup is not shown here; the Tailscale and OpenSSL references are rolling, and the self-signed fallback remains this guide's advice. -- auth (REASONED; compose, caddy, nginx-auth, traefik-auth, cloudflare-access, authelia-proxy): As this guide's assessment, proxy authentication and human MFA supplement application login. Caddy and nginx basic authentication reject missing credentials; nginx auth_request returns 401/403; Traefik basicAuth calls RequireAuth and forwardAuth passes through non-2xx; Cloudflare Access policies can require MFA, and Authelia can sit behind the proxy. Require MFA in the governing policy or portal; as this guide's assessment, basic auth alone is single-factor. The Caddy forward_auth rejection status, exact Traefik basicAuth status and Cloudflare Access anonymous status are not established by these fetched lines. -- secrets (REASONED; compose, docker-build): Use runtime environment files or Compose secrets, exclude .env from Git, and never put secrets in Dockerfile ENV/ARG; use temporary build secret mounts. Docker build and history behaviour is sourced; as this guide's assessment, the cited history reference does not establish that every argument value is shown; as this guide's assessment, never bake secrets into images and do not copy or print build-time secrets into the image. No direct source for the runtime delivery methods or the .env exclusion from Git is listed. -- workload (REASONED; compose, git-ignore, docker-workload): Keep .env in .gitignore, which ignores matching untracked files but not tracked files. Use a non-root Dockerfile USER; as this guide's assessment, a compromised app then is not root in the container. Drop capabilities, use no-new-privileges and a read-only root filesystem, and never mount the Docker socket. Docker sources establish the option names, that only trusted users should control the daemon, and that Unix permissions can limit access to its control socket; the linked guide's hardening consequences, including the mounted-socket path, remain this guide's assessment. -- database (REASONED; compose): Database TLS and authentication remain necessary for connections from outside the Compose network; database vendor sources are in linked guides. -- verify-publish (REASONED; compose, engine): Compose ps is project-scoped; inventory every host container and TCP/UDP socket, including IPv6. Socket visibility alone does not prove reachability. -- verify-redirect (REASONED; compose, caddy): Verify that HTTP redirects to HTTPS: Caddy v2.11.4 configures an automatic HTTPS redirect; as this guide's configuration-dependent expectation, nginx and Traefik require a configured redirect. This is an expectation, not an observed deployment result. -- verify-auth (REASONED; compose, caddy, nginx-auth, traefik-auth, authelia-proxy, cloudflare-access): Verify unauthenticated HTTPS /api rejection with printed status and redirect location; the probe itself fails on any 2xx. A 3xx passes only when the location is the gate's own login portal, never the application's own login; verify authorized success separately. Caddy basic auth and nginx auth_basic expect 401; Caddy forward_auth admits auth-service 2xx, but its rejection status is not established by the fetched lines. nginx auth_request returns 401 or 403, while the cited Authelia nginx configuration turns 401 into 302 to its portal. Traefik forwardAuth passes through a rejecting auth service's status, including a login redirect; Traefik basicAuth's 401 is this guide's expectation because the fetched code calls RequireAuth, whose response these lines do not show. Cloudflare Access's anonymous 302 login redirect is this guide's assessment per cloudflare.md, not established by the fetched Cloudflare lines. -- verify-isolation (REASONED; engine): Probe each restricted host address and publication from allowed/disallowed sources in both families; backend isolation must coexist with proxy reachability. -- firewalld-reload (REASONED; reload, reload-advisory): Engine 28.2.x and 28.3.0 through 28.3.2 fail to restore container-address filtering after firewalld reload; 28.3.3 fixes CVE-2025-54388 (GHSA-x4rx-4gw3-53p4). -- firewalld-scope (REASONED; reload, reload-advisory): The regression affects Linux Engine in the host network namespace with firewalld: routed remote hosts can reach published container ports, including loopback publications; unpublished ports stay filtered. Rootless Mode and Docker Desktop are unaffected. -- verify-firewalld-reload (REASONED; reload, reload-advisory): Confirm local backend health, then curl the container address from a second host with a bridge-subnet route before and after sudo firewall-cmd --reload. Without masking controls, affected hosts change from blocked to connectable; fixed hosts stay blocked. Any TCP connection, including HTTP 401/403, shows reachability; failure alone does not prove patching. Check route, target, IPv6, restricted host publications and continued proxy reachability. +```` - -Containers are where accidental exposure happens most. Two Docker behaviours cause it: - -1. `ports: - "3000:3000"` (or `-p 3000:3000`) publishes on every host address, `0.0.0.0` and `[::]`, at the time of writing. -2. On Linux, Docker programs iptables/nftables directly, so published ports are reachable **even when UFW or firewalld says the port is blocked**. A `ufw deny 3000` rule does not protect a published container port. - -## 1. Publish nothing except the TLS proxy - -Bind anything that must be reachable from the host to loopback, and give everything else no `ports:` entry at all; containers on the same Compose network reach each other by service name without published ports. - -```yaml -services: - app: - build: . - # no ports: entry; only the proxy is published - db: - image: postgres:17 - # no ports: entry; the app reaches it at db:5432 on the internal network -``` - -Where a host-published port is genuinely needed for local access: - -```yaml - ports: - - "127.0.0.1:3000:3000" -``` - -That host-IP restriction requires Docker Engine 28.0 or later, with the firewalld-reload caveat below. Before 28.0, under the default bridge configuration, a neighbour on the same layer-2 segment could reach a port mapped to a loopback address, a remote host could reach a container on a published port despite the host-IP binding, and an unpublished container port was reachable by routing directly to the container if the host's forwarding policy allowed it; 28.0 fixed all three. Check the running Engine version (`docker version --format '{{.Server.Version}}'`, not the client's); on an older engine, treat the address in a publish string as a convenience, not a boundary, and restrict the port in the `DOCKER-USER` chain below or upstream of the host (a cloud security group or a network ACL), since a host firewall like UFW or firewalld does not reach a published port (see the top of this guide). - -On Linux, when Docker Engine runs in the host's network namespace with firewalld running, Engine 28.2.x and 28.3.0 through 28.3.2 failed to restore container-address filtering rules after a firewalld reload. A remote host with a route to the bridge network could then reach published ports at container addresses, including loopback-only publications; unpublished ports remained filtered. Where firewalld is used, run Engine 28.3.3 or later, which fixes this regression (CVE-2025-54388, GHSA-x4rx-4gw3-53p4). Rootless Mode and Docker Desktop are unaffected by this regression. - -As this guide's assessment, where a port must be published on a routable interface, because other hosts need it but the whole internet does not, the most reliable restriction is upstream of the host, in a cloud security group or a network ACL: it does not depend on Docker's firewall backend, its userland proxy, or the host's boot order. See [cloud-firewalls.md](cloud-firewalls.md) for sourced AWS security-group, Google Cloud VPC firewall and Azure NSG rules; network-ACL configuration is not directly sourced here. On the host itself, filter it in the `DOCKER-USER` iptables chain (Docker's iptables backend only; its nftables backend has no such chain), whose rules Docker evaluates before its own accept rules. Match only new connections, so the rule does not also drop the replies to connections your own containers open, which arrive on the same interface and would otherwise lose their outbound access: - -```bash -# IPv4, run as root; replace ext_if with your external interface and the subnet with your own -iptables -I DOCKER-USER -i ext_if -m conntrack --ctstate NEW ! -s 192.0.2.0/24 -j DROP -``` - -This runs after destination NAT, so an ordinary destination match sees a container's internal IP and port, not the published host IP or port; it also covers every published port arriving on that interface, so scope it to one service with conntrack's `--ctorigdstport` and `--ctdir ORIGINAL` if you need to. As this guide's reasoning, `iptables` covers IPv4 only. Cover native IPv6 forwarding with an equivalent `ip6tables` `DOCKER-USER` rule, but note a second path: publishing without a host IP on an IPv4-only bridge with `--userland-proxy=true` (the default) also makes the port answer on the host's IPv6 addresses through Docker's userland proxy (it maps IPv6 to the container's IPv4). As this guide's reasoning, that connection terminates on a host process, so it traverses the host `INPUT` chain, not `FORWARD`, and a `DOCKER-USER` rule (which lives in `FORWARD`) never sees it. Close that path by publishing to a specific address (`-p 127.0.0.1:3000:3000` or a chosen IPv4), disabling the userland proxy, or restricting the host's IPv6 `INPUT`. Probe IPv4 and IPv6 separately, each from an allowed and a disallowed source. Confirm with `sudo iptables -nvL DOCKER-USER`, watching its counters, and by probing the port from a host outside the allowed subnet, expecting a timeout, and one inside it, expecting a connection. As this guide's assessment, the rule does not survive a reboot and is not in place while the host boots; persist just the DOCKER-USER rules with a startup unit ordered after the Docker service, not a blanket `iptables-persistent` save (which, as this guide's assessment, also captures Docker's own dynamic chains and can break container networking on the next boot), and where the boot-time window matters, rely on the upstream control. - -## 2. Terminate TLS in one proxy container - -Caddy is the least configuration ([caddy.md](caddy.md)); nginx ([nginx.md](nginx.md)) and Traefik ([traefik.md](traefik.md)) work the same way. A complete pattern: - -```yaml -services: - app: - build: . - - caddy: - image: caddy:2 - ports: - - "80:80" - - "443:443" - volumes: - - ./Caddyfile:/etc/caddy/Caddyfile:ro - - caddy_data:/data - - caddy_config:/config - -volumes: - caddy_data: - caddy_config: -``` - -`Caddyfile`: - -```caddyfile -app.example.com { - reverse_proxy app:3000 -} -``` - -Caddy obtains and renews the certificate automatically ([free-certificates.md](free-certificates.md) explains the ACME requirements). As this guide's recommendation, hosts with no inbound ports but a domain you can put on Cloudflare should use [cloudflare.md](cloudflare.md); run the `cloudflared` connector as a container on the app's Compose network and point it at `http://app:3000`. As this guide's recommendation, hosts with no domain at all should use [tailscale.md](tailscale.md) (Serve for private tailnet access or Funnel for public access), or [self-signed.md](self-signed.md) for internal use. Docker documents service-name lookup on the shared Compose network; the `http://app:3000` origin is this guide's inference from that lookup and [cloudflare.md](cloudflare.md)'s HTTP origin example, while the fetched Cloudflare origin parameters show an HTTPS service URL rather than this literal HTTP value. This guide does not show the tunnel-token setup; [cloudflare.md](cloudflare.md) gives the protected `--token-file` path. - -## 3. Authentication and secrets - -- As this guide's assessment, the proxy is the natural place for a first authentication gate (basic auth per the proxy guides, or Cloudflare Access); the application still needs its own login for anything multi-user ([authentication.md](authentication.md)). The proxy is also where MFA attaches for human-facing services ([mfa.md](mfa.md)): Cloudflare Access policies can require a second factor, and an Authelia portal can handle authentication behind Caddy `forward_auth`, nginx `auth_request` or Traefik `forwardAuth`. Require MFA in the governing policy or portal; as this guide's assessment, basic auth alone is single-factor. -- Pass secrets at runtime through environment files or Docker/Compose secrets. Never bake secrets into the image. Never put secrets in Dockerfile `ENV` or `ARG`: Docker documents that both are exposed in the final image; `ENV` also persists in containers from the image, and build arguments may persist in image metadata, provenance attestations and image history. As this guide's assessment, baking a key into an image ships it to every registry the image touches. `docker image history` shows the image's history, including creation commands; as this guide's assessment, the cited reference does not establish that every argument value is shown. For build-time secrets, use a temporary secret mount for the build instruction and do not copy or print the secret into the image. -- Keep `.env` in `.gitignore` (Git ignores matching untracked files, while already tracked files are unaffected), and run containers as a non-root user (`USER` in the Dockerfile); as this guide's assessment, this means a compromised app is not root in the container. As this guide's advice, non-root is the start, not the whole of workload hardening: dropping capabilities (`cap_drop`), `--security-opt no-new-privileges` (or Compose `security_opt`), a read-only root filesystem (Compose `read_only`), and never mounting the Docker socket are in [container-hardening.md](container-hardening.md). Docker documents these option names, that only trusted users should control the daemon, and that Unix permissions can limit access to its control socket; the linked guide's hardening consequences, including the mounted-socket path, remain this guide's assessment. -- Databases in containers still need their own TLS and authentication when anything outside the Compose network connects: see [postgresql.md](postgresql.md), [mysql.md](mysql.md), [mongodb.md](mongodb.md), and [redis.md](redis.md). - -## 4. Verify - -REASONED: publication, redirect, authentication and external isolation checks; no exposed/fixed run is recorded in this guide. Expectations follow the cited Docker networking sources and linked proxy guides; this read-only review has no authorized deployment or external probe hosts. For the unauthenticated API probe, expect 401 from Caddy or nginx basic auth; Caddy `forward_auth` admits auth-service 2xx, but its rejection status is not established by the fetched lines. Expect 401/403 from nginx `auth_request`; the cited Authelia nginx configuration converts its 401 to a 302 login redirect. Traefik `forwardAuth` passes through its auth service's non-2xx status, including a login redirect; Traefik `basicAuth` calls `RequireAuth`, whose response the fetched lines do not show, so 401 is this guide's expectation rather than a status established by the fetched middleware. For Cloudflare Access, this guide expects a 302 login redirect in the configuration described in [cloudflare.md](cloudflare.md); the fetched Cloudflare lines do not establish that status. A 3xx passes only when its printed location is the gate's own login portal (for Cloudflare Access, your team's `*.cloudflareaccess.com`), never the application's own login. The command fails on any 2xx; any status other than the gate's expected rejection or a 3xx to its portal is not a pass; and an authorized request must succeed separately. - -```bash -docker compose ps # project-scoped: only the proxy shows a published (0.0.0.0/[::] or a host IP) binding; review every container on the host, IPv6 included -ss -tulnp # host sockets (TCP and UDP); socket visibility varies by Docker version and config, so a socket listing alone does not prove reachability -curl -q -sI http://app.example.com/ # expect a redirect to https:// with Caddy automatic HTTPS, or after configuring nginx or Traefik to redirect -( set -o pipefail; curl -q -g -sS --noproxy '*' -o /dev/null -w 'http=%{http_code} loc=%{redirect_url}\n' https://app.example.com/api | awk '{ print; if ($1 ~ /^http=2/) { print "FAIL: unauthenticated /api returned " substr($1, 6); exit 1 } }' ) # unauthenticated: inspect the gate's rejection and portal location; then confirm an authorized request succeeds -``` - -REASONED: preceding block; publication and socket inventory, HTTPS redirect, unauthenticated API rejection and authorized API success follow the cited Docker networking sources and linked proxy guides, as do the external isolation probes this paragraph describes; no Docker runtime or external probe hosts are available, and no exposed/fixed result is recorded. Test from a second machine on a different network where possible; the UFW bypass means testing the firewall from the host itself proves nothing about published ports. The `curl` checks above exercise the proxy's redirect and auth, not the backend, so also probe each restricted host address and published port directly (for example port 3000) from an allowed and a disallowed source, IPv4 and IPv6 separately: a loopback-bound or firewalled backend must show unreachable while the proxy stays reachable. - -REASONED: this firewalld reload check follows the Impact section of GHSA-x4rx-4gw3-53p4 and the pinned v28.3.3 rule-restoration source. It has not been demonstrated here because the authoring environment has no Docker runtime, firewalld or external test host. For a loopback-published HTTP backend using default NAT bridge filtering, confirm that the backend answers locally. From a second host with a route through the Docker host to the bridge subnet, run `curl -q -g --noproxy '*' --connect-timeout 3 --max-time 5 -v http://172.17.0.2:3000/`, replacing the address and port with the actual container address and container port. Repeat with a fresh connection after running `sudo firewall-cmd --reload` on the Docker host. Without another control masking the regression, an affected host changes from blocked to connectable; a fixed host remains blocked. Any successful TCP connection, including an HTTP 401 or 403 response, demonstrates reachability. A failed probe alone does not establish that the Engine is patched: confirm the route, target and backend health. Repeat for configured IPv6 addresses and the restricted host-address publications, and confirm that the TLS proxy remains reachable. - -## Sources (checked September 2026) - -- Docker build variables (`ENV` persists in containers; build arguments may persist in metadata, provenance and history), at 4e9a5751518ed8223a8dcde53693badddd72604f: https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/build/building/variables.md#L17-L52 -- Docker build secrets (temporary mounts for build instructions), at 4e9a5751518ed8223a8dcde53693badddd72604f: https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/build/building/secrets.md#L13-L22 -- Docker image history command (image history and creation-command output), at 4e9a5751518ed8223a8dcde53693badddd72604f: https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_image_history.yaml#L1-L118 -- Git v2.49.0 ignore rules for untracked and tracked files (checked October 2026): https://github.com/git/git/blob/v2.49.0/Documentation/gitignore.adoc#L15-L18 -- Docker Dockerfile `USER` instruction, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/_vendor/github.com/moby/buildkit/frontend/dockerfile/docs/reference.md#L2448-L2463 -- Docker Compose `cap_drop` option, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L224-L233 -- Docker Compose `read_only` option, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L1892-L1894 -- Docker Compose `security_opt` and `no-new-privileges` spellings, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L2008-L2021 -- Docker run `--security-opt no-new-privileges`, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L2495-L2502 -- Docker daemon control-socket access and host filesystem control, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/_index.md#L84-L112 -- Docker iptables and ip6tables daemon options, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/firewall-iptables.md#L8-L15 -- Docker iptables DOCKER-USER and FORWARD chains, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/firewall-iptables.md#L17-L42 -- Docker IPv6-to-IPv4 userland-proxy mapping, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L60-L62 -- Docker port publishing with an explicit host address, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L33-L38 -- Docker firewall rules and warning against modifying Docker-created rules, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15 -- Moby Docker service unit, at v28.3.3 (checked October 2026): https://github.com/moby/moby/blob/v28.3.3/contrib/init/systemd/docker.service#L1-L9 -- systemd After= unit ordering, at v262 (checked October 2026): https://github.com/systemd/systemd/blob/v262/man/systemd.unit.xml#L826-L838 -- Docker packet filtering and firewalls: https://docs.docker.com/engine/network/packet-filtering-firewalls/ -- Docker with iptables, for the `DOCKER-USER` chain (processed before Docker's own rules; matches container addresses after DNAT): https://docs.docker.com/engine/network/firewall-iptables/ -- Docker Engine 28.0 release notes, for the published-port and loopback-mapping hardening: https://docs.docker.com/engine/release-notes/28/ -- Docker Engine 28.3.3 security fix: https://docs.docker.com/engine/release-notes/28/#2833 and affected-version advisory (OSV copy of the July 2025 Moby advisory): https://osv.dev/vulnerability/GHSA-x4rx-4gw3-53p4 -- Moby 28.3.3 (pinned tag v28.3.3), `reapplyPerPortIptables` restores endpoint rules after firewalld reload: https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go -- Compose networking: https://docs.docker.com/compose/how-tos/networking/ -- Docker port publishing (with no host address, "the Docker daemon publishes ports to all host addresses (0.0.0.0 and [::])"): https://docs.docker.com/engine/network/port-publishing/ -- PostgreSQL 17 connection settings and default TCP port (checked October 2026): https://www.postgresql.org/docs/17/runtime-config-connection.html -- Caddy image fba2853501d36e8a72f946ac8cb7ff64d07e48f2 data/config paths, exposed ports and Caddyfile command (checked October 2026): https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L44-L45, https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L56-L59, https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L63 -- Caddy v2.11.4 automatic certificate management and reverse_proxy syntax (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L62-L67, https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L60 -- Caddy v2.11.4 authentication handler and 401 rejection (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/caddyauth/caddyauth.go#L84-L120 -- Caddy v2.11.4 basic-auth missing-credential path (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/caddyauth/basicauth.go#L142-L163 -- Caddy v2.11.4 forward_auth accepted-response handler (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go#L174-L194 -- Caddy v2.11.4 automatic redirect setup (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L234-L240 -- Caddy v2.11.4 HTTPS redirect route (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L544-L575 -- nginx release-1.26.3 missing basic-auth credentials (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_basic_module.c#L122-L129 -- nginx release-1.26.3 basic-auth 401 challenge (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_basic_module.c#L317-L348 -- nginx release-1.26.3 auth_request status handling (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_request_module.c#L135-L182 -- Traefik v3.5.0 basicAuth failure path (checked October 2026): https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/basic_auth.go#L67-L99 -- Traefik v3.5.0 forwardAuth keeps the auth service redirect (checked October 2026): https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/forward.go#L93-L99 -- Traefik v3.5.0 forwardAuth rejection status passthrough (checked October 2026): https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/forward.go#L233-L262 -- Cloudflare Access MFA policy enforcement (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L17-L85 -- Cloudflare Access self-hosted application protection (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54 -- Authelia v4.39.28 supported proxy integration (checked October 2026): https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L24-L32 -- Authelia v4.39.28 nginx portal redirect (checked October 2026): https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L537-L541 -- AWS security-group rules (rolling reference reused from `cloud-firewalls.md`; not rechecked here): https://docs.aws.amazon.com/vpc/latest/userguide/security-group-rules.html -- Google Cloud VPC firewall rules (rolling reference reused from `cloud-firewalls.md`; not rechecked here): https://docs.cloud.google.com/firewall/docs/using-firewalls -- Azure NSG rule properties (rolling reference reused from `cloud-firewalls.md`; not rechecked here): https://learn.microsoft.com/en-us/rest/api/virtualnetwork/network-security-groups/get -- cloudflared 2026.9.3 tunnel daemon without firewall openings (checked October 2026): https://github.com/cloudflare/cloudflared/blob/2026.9.3/README.md#L1-L5 -- cloudflared 2026.9.3 Docker image (checked October 2026): https://github.com/cloudflare/cloudflared/blob/2026.9.3/README.md#L26-L33 -- cloudflared 2026.9.3 container entry point (checked October 2026): https://github.com/cloudflare/cloudflared/blob/2026.9.3/Dockerfile#L40-L42 -- Cloudflare Tunnel origin service URL example at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/partials/cloudflare-one/tunnel/origin-parameters.mdx#L31-L49 -- Cloudflare Tunnel published application domain prerequisite at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel.mdx#L27-L37 -- Docker Compose service-name lookup at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/compose/how-tos/networking.md#L13-L17 -- Tailscale Serve private tailnet entry (rolling reference reused from `tailscale.md`; not rechecked here): https://tailscale.com/kb/1242/tailscale-serve -- Tailscale Funnel public entry (rolling reference reused from `tailscale.md`; not rechecked here): https://tailscale.com/kb/1223/funnel -- OpenSSL documentation index (rolling reference reused from `self-signed.md`; not rechecked here): https://docs.openssl.org/ diff --git a/firebase-supabase.md b/firebase-supabase.md index 89bd0b9fe..46d251875 100644 --- a/firebase-supabase.md +++ b/firebase-supabase.md @@ -1,308 +1,25 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-19", - "documentation_checked": "2026-09", - "body_sha256": "6690687db9e1d66f3e67a898fc0b030022e55be24166bc29b8c35554e8081891", - "components": { - "firebase": { - "name": "Firebase documentation", - "basis": "unknown", - "sources": { - "s100c2cdbdd59": "https://firebase.google.com/docs/rules", - "s53bfe8b21868": "https://firebase.google.com/docs/rules/basics", - "s622d3bb0665b": "https://firebase.google.com/docs/firestore/quickstart", - "sb2c933c841ef": "https://firebase.google.com/docs/database/web/start", - "s75f018e0fac7": "https://firebase.google.com/support/releases", - "s7bd11c22c7ce": "https://firebase.google.com/docs/firestore/security/rules-structure", - "sa6b1c4933e6a": "https://firebase.google.com/docs/database/security/core-syntax", - "s0a7f54463abd": "https://firebase.google.com/docs/storage/security/core-syntax", - "sd6071548f6fe": "https://firebase.google.com/docs/firestore/security/get-started", - "s14872143a9e8": "https://firebase.google.com/docs/storage/security/get-started", - "sc8f214d059d2": "https://firebase.google.com/docs/functions/callable", - "s79da8f65f90a": "https://firebase.google.com/docs/functions/callable-reference", - "s9a86df9623a6": "https://firebase.google.com/docs/functions/1st-gen/callable-1st", - "s49f78d054708": "https://firebase.google.com/docs/auth/admin/verify-id-tokens", - "s1bb711c9afeb": "https://firebase.google.com/docs/functions/http-events", - "sc9f86ea271f6": "https://firebase.google.com/docs/reference/functions/2nd-gen/node/firebase-functions.https.httpsoptions", - "sb0ff8f1726f7": "https://firebase.google.com/docs/app-check/cloud-functions", - "sa71e958c99df": "https://firebase.google.com/docs/app-check/web/recaptcha-enterprise-provider", - "sba7129c83405": "https://firebase.google.com/docs/app-check/enable-enforcement", - "safeed4cd2b67": "https://firebase.google.com/docs/projects/api-keys", - "sc78d5eb59b65": "https://docs.cloud.google.com/api-keys/docs/add-restrictions-api-keys", - "sb0d26775f3b4": "https://firebase.google.com/docs/web/learn-more", - "s77e2cbd8a29a": "https://firebase.google.com/docs/auth/web/anonymous-auth", - "s06808e4b6965": "https://docs.cloud.google.com/identity-platform/docs/admin/email-enumeration-protection", - "sd8a1bda29f5c": "https://firebase.google.com/docs/emulator-suite/install_and_configure", - "s69d9721c7f29": "https://firebase.google.com/docs/emulator-suite/use_hosting", - "s8c90f9f6c7fa": "https://firebase.google.com/docs/reference/js/auth", - "s8c8336ec252e": "https://firebase.google.com/docs/reference/js/auth.additionaluserinfo", - "s142e56472093": "https://firebase.google.com/docs/auth/admin/manage-sessions", - "s9b7e57b472c7": "https://firebase.google.com/docs/storage/admin/start", - "sa0584b797732": "https://firebase.google.com/docs/reference/kotlin/com/google/firebase/storage/StorageReference", - "sfd1e7412abf6": "https://firebase.google.com/docs/storage/web/download-files" - } - }, - "supabase": { - "name": "Supabase documentation", - "basis": "unknown", - "sources": { - "s73d939b4644e": "https://supabase.com/docs/guides/api/api-keys", - "s46cff2590323": "https://supabase.com/docs/guides/database/postgres/row-level-security", - "s741468ff35a9": "https://supabase.com/docs/guides/auth/auth-mfa", - "s6d5cf2d4dbf3": "https://supabase.com/docs/guides/api/securing-your-api", - "s20ef046ac646": "https://supabase.com/docs/guides/api/using-custom-schemas", - "sa946354db383": "https://supabase.com/docs/guides/getting-started/migrating-to-new-api-keys", - "sfc2f9ae0232b": "https://supabase.com/docs/guides/auth/signing-keys", - "s8405a9fb4b74": "https://supabase.com/docs/guides/database/functions", - "s13da01bbbba5": "https://supabase.com/docs/guides/database/extensions/http", - "s76740bffcb32": "https://supabase.com/docs/guides/database/extensions/pg_net", - "s47bb8fa3e14e": "https://supabase.com/docs/guides/database/vault", - "s1567fb838f20": "https://supabase.com/docs/guides/storage/buckets/fundamentals", - "sdba7d7b6dcf8": "https://supabase.com/docs/guides/storage/security/access-control", - "s9e1484f46eff": "https://supabase.com/docs/guides/storage/schema/helper-functions", - "sc29febbb230e": "https://supabase.com/docs/guides/storage/serving/downloads", - "see2d3be99ebd": "https://supabase.com/docs/guides/storage/uploads/standard-uploads", - "s5674b1057e14": "https://supabase.com/docs/guides/realtime/settings", - "s84efad0ba39d": "https://supabase.com/docs/guides/realtime/authorization", - "s694eab64f2ec": "https://supabase.com/docs/guides/realtime/postgres-changes", - "sc797f0b63472": "https://supabase.com/docs/guides/auth/general-configuration", - "s3a5e39fe7b4d": "https://supabase.com/docs/guides/local-development/cli/config", - "sa2dd4637969e": "https://supabase.com/docs/guides/auth/passwords", - "sad79d4d1b6ee": "https://supabase.com/docs/guides/auth/auth-anonymous", - "s182755725d74": "https://supabase.com/docs/guides/auth/auth-mfa/totp", - "s92b405dc3c7e": "https://supabase.com/docs/reference/javascript/select", - "s087c8cf42ab0": "https://supabase.com/docs/reference/javascript/rpc", - "sf3246532dd18": "https://supabase.com/docs/guides/functions", - "s7dfbb47b5d20": "https://supabase.com/docs/reference/cli/supabase-functions-deploy", - "sf7d438b8c080": "https://supabase.com/docs/guides/functions/auth", - "se52bbd097033": "https://supabase.com/docs/guides/functions/auth-headers", - "sb3c4c0993e14": "https://supabase.com/docs/guides/functions/secrets", - "sb787b5d63633": "https://supabase.com/docs/guides/platform/network-restrictions", - "s83e15676e1ff": "https://supabase.com/docs/guides/platform/ssl-enforcement", - "sf69cafb4bc6c": "https://supabase.com/docs/guides/database/postgres/roles" - } - }, - "postgres": { - "name": "PostgreSQL documentation", - "basis": "unknown", - "sources": { - "s13c54ab9cc6b": "https://www.postgresql.org/docs/current/sql-createview.html", - "saa99ef28f60f": "https://www.postgresql.org/docs/current/sql-alterview.html", - "sb6224f4ac866": "https://www.postgresql.org/docs/current/sql-revoke.html", - "se9846041ce1f": "https://www.postgresql.org/docs/current/ddl-rowsecurity.html", - "s8e65a2f3f2bb": "https://www.postgresql.org/docs/current/sql-createpolicy.html", - "sec39c317b06f": "https://www.postgresql.org/docs/current/sql-createfunction.html", - "s0fe66d71aa10": "https://www.postgresql.org/docs/current/sql-alterfunction.html", - "s04e81888353f": "https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html", - "sc39eceeac711": "https://www.postgresql.org/docs/current/functions-info.html" - } - }, - "postgres-view": { - "name": "PostgreSQL invoker-view minimum", - "basis": "15", - "sources": { - "s14443e063ef1": "https://www.postgresql.org/docs/release/15.0/" - } - }, - "curl": { - "name": "curl", - "basis": "curl-8_14_1", - "sources": { - "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", - "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md" - } - }, - "shellcheck": { - "name": "ShellCheck", - "basis": "v0.11.0", - "sources": { - "sf994764bf270": "https://github.com/koalaman/shellcheck/blob/v0.11.0/CHANGELOG.md#L1" - } - } - }, - "claims": { - "public-config": {"text": "Firebase API keys/config and Supabase publishable/anon keys are public identifiers, not authorization; hosted endpoints handle TLS.", "components": ["firebase", "supabase"], "sources": ["firebase:safeed4cd2b67", "firebase:sb0d26775f3b4", "supabase:s73d939b4644e"], "status": "REASONED"}, - "firebase-ownership": {"text": "Require authentication and ownership separately in Firestore, RTDB and Storage; authentication-only rules do not isolate users.", "components": ["firebase"], "sources": ["firebase:s100c2cdbdd59", "firebase:s53bfe8b21868"], "status": "REASONED"}, - "firebase-starting": {"text": "Firestore production and RTDB locked modes deny clients; test templates expose data for about a month. Inspect expiry and replace open rules.", "components": ["firebase"], "sources": ["firebase:s622d3bb0665b", "firebase:sb2c933c841ef", "firebase:s75f018e0fac7"], "status": "REASONED"}, - "firestore-rules": {"text": "Rules version 2 recursive wildcards include the user document and descendants; version 1 excludes the parent. Unmatched paths deny and overlapping grants can reopen access.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, - "rtdb-rules": {"text": "Owner rules under users.$uid need ancestor grants removed; child rules cannot revoke parent grants or filter parent reads.", "components": ["firebase"], "sources": ["firebase:sa6b1c4933e6a"], "status": "REASONED"}, - "firebase-storage": {"text": "Storage has separate rules; conflicting default prose/example warrants explicit deny-all followed by deliberate owner-path access.", "components": ["firebase"], "sources": ["firebase:s53bfe8b21868", "firebase:s0a7f54463abd"], "status": "REASONED"}, - "firebase-download": {"text": "Storage download URLs are non-expiring bearer credentials unaffected by tighter Rules; revoke tokens and use authorized getBlob/getBytes direct downloads.", "components": ["firebase"], "sources": ["firebase:s9b7e57b472c7", "firebase:sa0584b797732", "firebase:sfd1e7412abf6"], "status": "REASONED"}, - "firebase-rule-deploy": {"text": "Inspect and test deployed rules for every database/bucket/instance; keep local and deployed definitions synchronized.", "components": ["firebase"], "sources": ["firebase:sd6071548f6fe", "firebase:s14872143a9e8"], "status": "REASONED"}, - "firebase-server": {"text": "Admin SDK/server credentials bypass client Rules; keep credentials server-side and apply IAM and caller authorization.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, - "firebase-callable": {"text": "Callable ID-token validation is optional authentication; reject missing request.auth, use its UID and validate resource access. First-generation handlers use context.auth.", "components": ["firebase"], "sources": ["firebase:s79da8f65f90a", "firebase:sc8f214d059d2", "firebase:s9a86df9623a6"], "status": "REASONED"}, - "firebase-callable-appcheck": {"text": "onCall enforceAppCheck needs firebase-functions 4.0.0 or newer; App Check does not replace caller/resource authorization.", "components": ["firebase"], "sources": ["firebase:sb0ff8f1726f7"], "status": "REASONED"}, - "firebase-http": {"text": "onRequest does not inherit callable auth; verify end-user tokens with verifyIdToken or restrict IAM invokers, then authorize the intended principal.", "components": ["firebase"], "sources": ["firebase:s49f78d054708", "firebase:s1bb711c9afeb", "firebase:sc9f86ea271f6"], "status": "REASONED"}, - "firebase-appcheck": {"text": "Register compatible clients and inspect metrics, then enforce App Check per product and per callable; registration alone enforces nothing.", "components": ["firebase"], "sources": ["firebase:sa71e958c99df", "firebase:sba7129c83405", "firebase:sb0ff8f1726f7"], "status": "REASONED"}, - "firebase-key-restrictions": {"text": "Apply one app-restriction type per key plus API restrictions; separate platforms and unrelated billable APIs, using the full Firebase product table.", "components": ["firebase"], "sources": ["firebase:sc78d5eb59b65", "firebase:safeed4cd2b67"], "status": "REASONED"}, - "firebase-anonymous": {"text": "Anonymous sign-in is opt-in; disable unused providers because anonymous accounts still satisfy authentication-only Rules.", "components": ["firebase"], "sources": ["firebase:s77e2cbd8a29a"], "status": "REASONED"}, - "firebase-enumeration": {"text": "Email enumeration protection defaults on only for projects created from September 15, 2023; signup can still return EMAIL_EXISTS.", "components": ["firebase"], "sources": ["firebase:s06808e4b6965"], "status": "REASONED"}, - "firebase-emulators": {"text": "Emulators default to localhost but missing Firestore/RTDB/Storage Rules configuration means open data; keep listeners, UI, hub, proxies and tunnels private.", "components": ["firebase"], "sources": ["firebase:sd8a1bda29f5c", "firebase:s69d9721c7f29"], "status": "REASONED"}, - "supabase-network": {"text": "Restrict hosted database/pooler client IPs with Network Restrictions, enforce SSL and protect database-role passwords.", "components": ["supabase"], "sources": ["supabase:sb787b5d63633", "supabase:s83e15676e1ff", "supabase:sf69cafb4bc6c"], "status": "REASONED"}, - "supabase-grants": {"text": "Data API access needs grants plus RLS; automatic-grant defaults are transitioning. Table Editor enables RLS, while SQL creation needs explicit enablement.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3"], "status": "REASONED"}, - "supabase-select": {"text": "The profiles baseline enables RLS, revokes broad grants and permits authenticated owners SELECT; audit inherited/column grants and existing policies.", "components": ["supabase", "postgres"], "sources": ["supabase:s46cff2590323", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, - "supabase-bypass": {"text": "With RLS enabled and no policy, ordinary subject roles get no rows; owners normally bypass, as do superusers/BYPASSRLS roles. TRUNCATE is outside RLS.", "components": ["postgres"], "sources": ["postgres:se9846041ce1f"], "status": "REASONED"}, - "supabase-writes": {"text": "Grant and policy INSERT/UPDATE/DELETE only when intended; UPDATE checks old/new ownership and needs applicable SELECT when reading columns.", "components": ["supabase", "postgres"], "sources": ["supabase:s46cff2590323", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, - "supabase-schemas": {"text": "public is exposed by default; restrict exposed schemas and deliberately grant schema/object access. Schema selection does not replace RLS.", "components": ["supabase"], "sources": ["supabase:s20ef046ac646"], "status": "REASONED"}, - "supabase-api-disable": {"text": "Disable an unused Data API to close generated REST endpoints; this does not disable all other services.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3"], "status": "REASONED"}, - "supabase-keys": {"text": "Publishable keys select anon or authenticated by session; secret and legacy service_role keys bypass RLS and belong only on trusted servers.", "components": ["supabase"], "sources": ["supabase:s73d939b4644e"], "status": "REASONED"}, - "supabase-key-retire": {"text": "Replacement keys do not disable legacy keys; migrate consumers then explicitly deactivate legacy keys, or delete compromised secret keys.", "components": ["supabase"], "sources": ["supabase:sa946354db383"], "status": "REASONED"}, - "supabase-jwt-retire": {"text": "Rotation changes the signing key but retains previous trust until revocation; account for verifier caches and propagation.", "components": ["supabase"], "sources": ["supabase:sfc2f9ae0232b"], "status": "REASONED"}, - "supabase-view-owner": {"text": "Views use owner privileges/policies by default; disclosure depends on reachable schemas, grants, query and owner privileges, not anon-key possession alone.", "components": ["postgres", "supabase"], "sources": ["postgres:s13c54ab9cc6b", "supabase:s46cff2590323"], "status": "REASONED"}, - "supabase-view-invoker": {"text": "PostgreSQL 15+ security_invoker views apply caller policies and require underlying privileges.", "components": ["postgres-view", "postgres"], "sources": ["postgres-view:s14443e063ef1", "postgres:saa99ef28f60f", "postgres:s13c54ab9cc6b"], "status": "REASONED"}, - "supabase-view-legacy": {"text": "PostgreSQL 14 and earlier require revoking all view privileges from PUBLIC/anon/authenticated or unexposing the schema; updatable views can bypass RLS for writes.", "components": ["postgres"], "sources": ["postgres:s13c54ab9cc6b", "postgres:sb6224f4ac866", "postgres:sc39eceeac711"], "status": "REASONED"}, - "supabase-functions": {"text": "Prefer INVOKER; restrict DEFINER execution, use empty search_path with qualified references and enforce caller authorization including required MFA.", "components": ["supabase", "postgres"], "sources": ["supabase:s8405a9fb4b74", "postgres:sec39c317b06f", "postgres:s0fe66d71aa10"], "status": "REASONED"}, - "supabase-function-defaults": {"text": "New functions grant PUBLIC execution; create privileged functions and restrict grants transactionally. Global/default revocations are creator-specific and do not fix existing functions.", "components": ["postgres"], "sources": ["postgres:sec39c317b06f", "postgres:s04e81888353f"], "status": "REASONED"}, - "supabase-http": {"text": "Audit http/pg_net wrappers and revoke unneeded execute grants; constrain callers/destinations/data. Extension enablement alone does not prove exploitable SSRF.", "components": ["supabase"], "sources": ["supabase:s13da01bbbba5", "supabase:s76740bffcb32"], "status": "REASONED"}, - "supabase-vault": {"text": "Restrict vault.decrypted_secrets and privileged wrappers and keep Vault unexposed; at-rest encryption cannot prevent authorized plaintext queries.", "components": ["supabase"], "sources": ["supabase:s47bb8fa3e14e"], "status": "REASONED"}, - "supabase-buckets": {"text": "Private downloads need authorized access or time-limited signed URLs; public buckets bypass retrieval checks but not upload/delete/move/copy controls.", "components": ["supabase"], "sources": ["supabase:s1567fb838f20"], "status": "REASONED"}, - "supabase-storage-policies": {"text": "Authorize storage.objects separately by bucket and owner path; upload needs INSERT, upsert also SELECT/UPDATE, and deletion needs deliberate permission.", "components": ["supabase"], "sources": ["supabase:sdba7d7b6dcf8", "supabase:s9e1484f46eff", "supabase:see2d3be99ebd"], "status": "REASONED"}, - "supabase-realtime-default": {"text": "Public Realtime channels are allowed by default; disable them project-wide and also set client config.private.", "components": ["supabase"], "sources": ["supabase:s5674b1057e14"], "status": "REASONED"}, - "supabase-realtime-policy": {"text": "Broadcast/Presence receive/send need separate SELECT/INSERT policies on realtime.messages, constrained by topic ownership and extension.", "components": ["supabase"], "sources": ["supabase:s84efad0ba39d"], "status": "REASONED"}, - "supabase-changes": {"text": "Postgres Changes uses source-table authorization; private-channel policies are not universal and DELETE has separate limitations.", "components": ["supabase"], "sources": ["supabase:s694eab64f2ec"], "status": "REASONED"}, - "supabase-signup": {"text": "Disable signup, anonymous sign-ins and unused providers when unwanted; inspect hosted settings instead of assuming universal defaults.", "components": ["supabase"], "sources": ["supabase:sc797f0b63472"], "status": "REASONED"}, - "supabase-cli-auth": {"text": "CLI enable_signup defaults true and enable_anonymous_sign_ins false; local config edits do not prove hosted settings changed.", "components": ["supabase"], "sources": ["supabase:s3a5e39fe7b4d"], "status": "REASONED"}, - "supabase-mfa": {"text": "TOTP is available on every plan at writing; enrollment alone does not constrain data. Require aal2 using a restrictive policy alongside permissive ownership policies.", "components": ["supabase", "postgres"], "sources": ["supabase:s182755725d74", "supabase:s741468ff35a9", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, - "supabase-edge-jwt": {"text": "Per-function verify_jwt defaults true; --no-verify-jwt disables platform checking and requires handler authentication before protected work.", "components": ["supabase"], "sources": ["supabase:s7dfbb47b5d20", "supabase:sf7d438b8c080", "supabase:se52bbd097033"], "status": "REASONED"}, - "supabase-edge-auth": {"text": "Platform JWT success is not resource authorization and API-key compatibility can pass it; separately authorize users or verify webhook signatures.", "components": ["supabase"], "sources": ["supabase:sf7d438b8c080", "supabase:se52bbd097033"], "status": "REASONED"}, - "supabase-edge-secrets": {"text": "Edge environments contain privileged credentials including SUPABASE_SERVICE_ROLE_KEY; protect managed/local secrets, responses, logs and client bundles.", "components": ["supabase"], "sources": ["supabase:sb3c4c0993e14"], "status": "REASONED"}, - "verify-firestore": {"text": "Compare unsigned/A/B reads and writes to user documents and descendants, with a successful owner control; test parent documents to distinguish recursive-rule versions.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, - "verify-rtdb": {"text": "Compare unsigned/A/B reads/writes to seeded RTDB owner paths; ancestor grants expose them while ownership rules deny unauthorized callers.", "components": ["firebase"], "sources": ["firebase:sa6b1c4933e6a"], "status": "REASONED"}, - "verify-firebase-storage": {"text": "Compare unsigned/A/B object reads and uploads; ownership rules must deny unrelated callers while owner operations succeed.", "components": ["firebase"], "sources": ["firebase:s0a7f54463abd"], "status": "REASONED"}, - "verify-download-token": {"text": "Same sessionless download URL works after Rules tightening but must lose bytes after token revocation; owner getBytes remains successful and network/CORS failures are inconclusive.", "components": ["firebase"], "sources": ["firebase:s9b7e57b472c7", "firebase:sa0584b797732", "firebase:sfd1e7412abf6"], "status": "REASONED"}, - "verify-tables": {"text": "Pair unsigned/A/B profiles GETs against seeded rows, using ordinary credentials and required MFA; test intended writes and persisted effects separately.", "components": ["supabase", "postgres", "curl"], "sources": ["supabase:s92b405dc3c7e", "postgres:se9846041ce1f", "supabase:s741468ff35a9", "curl:s5e1383feef7d", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, - "verify-views": {"text": "Compare view reads with table boundaries; owner-privileged exposure must close under invoker/revoked access while intended reads or deliberate removal are confirmed.", "components": ["postgres"], "sources": ["postgres:s13c54ab9cc6b"], "status": "REASONED"}, - "verify-rpc": {"text": "Test every API function overload with valid harmless arguments as unsigned/A/B; unauthorized data or operations must be denied while the intended caller succeeds.", "components": ["supabase"], "sources": ["supabase:s087c8cf42ab0", "supabase:s8405a9fb4b74"], "status": "REASONED"}, - "verify-http": {"text": "Probe HTTP wrappers with a controlled collector and marker; unauthorized fixed calls must emit no request. Commit pg_net fixture transactions before expecting delivery.", "components": ["supabase"], "sources": ["supabase:s76740bffcb32"], "status": "REASONED"}, - "verify-vault": {"text": "Use a disposable Vault canary to compare exposed/fixed wrappers; inspect function and table privileges as supporting evidence.", "components": ["supabase", "postgres"], "sources": ["supabase:s47bb8fa3e14e", "postgres:sc39eceeac711"], "status": "REASONED"}, - "verify-storage": {"text": "Compare public URL and A/B private downloads plus intended uploads/upserts; unauthorized callers must receive no bytes or cross-owner writes.", "components": ["supabase"], "sources": ["supabase:sc29febbb230e", "supabase:see2d3be99ebd", "supabase:s1567fb838f20"], "status": "REASONED"}, - "verify-realtime": {"text": "Compare A/B join, Broadcast and Presence delivery; public-disabled clients lacking private=true must fail, and connection alone is insufficient.", "components": ["supabase"], "sources": ["supabase:s5674b1057e14", "supabase:s84efad0ba39d"], "status": "REASONED"}, - "verify-changes": {"text": "Observe A/B INSERT/UPDATE subscription events against seeded rows; fixed table authorization withholds unrelated events. Evaluate DELETE separately.", "components": ["supabase"], "sources": ["supabase:s694eab64f2ec"], "status": "REASONED"}, - "verify-edge": {"text": "Compare no/invalid Bearer without apikey, authorized owners and valid webhook signatures; distinguish platform 401 from handler authorization and inspect effects/secrets.", "components": ["supabase"], "sources": ["supabase:se52bbd097033", "supabase:sf7d438b8c080", "supabase:sb3c4c0993e14"], "status": "REASONED"}, - "verify-firebase-functions": {"text": "Compare callable requests with valid App Check but missing user auth and A/B operations; HTTP endpoints need separate invalid/valid user or IAM comparisons.", "components": ["firebase"], "sources": ["firebase:s79da8f65f90a", "firebase:s49f78d054708"], "status": "REASONED"}, - "verify-appcheck": {"text": "Repeat owner requests with/without App Check per product/callable; enforcement must deny missing attestation while compatible clients work.", "components": ["firebase"], "sources": ["firebase:sba7129c83405", "firebase:sb0ff8f1726f7"], "status": "REASONED"}, - "verify-api-restrictions": {"text": "Test intended versus excluded apps/referrers after key restrictions, retaining successful intended product traffic.", "components": ["firebase"], "sources": ["firebase:safeed4cd2b67", "firebase:sc78d5eb59b65"], "status": "REASONED"}, - "verify-emulators": {"text": "Enumerate hub /emulators and sockets, compare missing versus loaded Rules and second-host connectivity, and retain local owner controls.", "components": ["firebase"], "sources": ["firebase:sd8a1bda29f5c", "firebase:s69d9721c7f29"], "status": "REASONED"}, - "verify-firebase-accounts": {"text": "Sign out before each Anonymous signup test and check isNewUser; disabled providers must reject creation. Enumeration tests compare sign-in errors, not EMAIL_EXISTS signup behavior.", "components": ["firebase"], "sources": ["firebase:s8c90f9f6c7fa", "firebase:s8c8336ec252e", "firebase:s142e56472093", "firebase:s77e2cbd8a29a", "firebase:s06808e4b6965"], "status": "REASONED"}, - "verify-supabase-accounts": {"text": "Fresh signup and anonymous creation must fail under invite-only/anonymous-disabled settings while an invited account still signs in.", "components": ["supabase"], "sources": ["supabase:sc797f0b63472", "supabase:sa2dd4637969e", "supabase:sad79d4d1b6ee"], "status": "REASONED"}, - "verify-mfa": {"text": "Owner profiles GET with aal1 must yield no protected rows under the restrictive policy, while fresh aal2 returns the seeded owner row.", "components": ["supabase"], "sources": ["supabase:s741468ff35a9"], "status": "REASONED", "verify": [1]}, - "verify-key-retirement": {"text": "Compare old/new API keys after explicit deactivation and old/new JWTs after revocation, accounting for propagation and caches.", "components": ["supabase"], "sources": ["supabase:sa946354db383", "supabase:sfc2f9ae0232b"], "status": "REASONED"}, - "verify-api-removal": {"text": "Previously successful profiles requests must stop returning fixtures after schema removal or Data API disabling; retained schemas need authorized controls.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3", "supabase:s20ef046ac646"], "status": "REASONED"}, - "verify-bundle": {"text": "Filename-only service_role/eyJ/sb_secret_ scanning yields review candidates, not proof of leaks; public anon JWTs are legitimate. Use a marker control and inspect encoded/split secrets separately.", "components": ["supabase"], "sources": ["supabase:s73d939b4644e"], "status": "REASONED", "verify": [2]}, - "local-guards": {"text": "Historical local guard tests rejected placeholders and malformed set lines; this guide's assessment is shell behavior only, not live authorization. The recorded ShellCheck 0.11.0 has a release citation, not host-binary evidence; bash and other tool/parser versions remain unrecorded.", "components": ["supabase", "shellcheck"], "sources": ["supabase:s92b405dc3c7e", "supabase:s73d939b4644e", "shellcheck:sf994764bf270"], "status": "DEMONSTRATED", "evidence": "Local guard tests refused embedded `REPLACE_WITH_` placeholders, `example.com`, angle brackets, empty values, and omitted or shortened `set --` lines."}, - "local-syntax": {"text": "Recorded ShellCheck 0.11.0/bash and JSON/TOML/JavaScript parsing passed; this guide's assessment is that these checks do not compile Firebase Rules, execute SQL or demonstrate services. Only the ShellCheck release is cited; bash and other tool/parser versions remain unrecorded.", "components": ["firebase", "supabase", "shellcheck"], "sources": ["firebase:sa6b1c4933e6a", "firebase:sd8a1bda29f5c", "supabase:s3a5e39fe7b4d", "firebase:sc8f214d059d2", "shellcheck:sf994764bf270"], "status": "DEMONSTRATED", "evidence": "The RTDB JSON, `firebase.json` fragment, Auth TOML, and callable JavaScript passed local parsing checks."} - } -} ---- # Firebase and Supabase: the rules are the security - -**Version basis** +Written against Firebase documentation (no recorded version), PostgreSQL invoker-view minimum 15, curl curl-8_14_1, and ShellCheck v0.11.0; 2 further sources carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 70 claims: 2 demonstrated by a check that ran and 68 reasoned from cited sources. -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +These platforms handle TLS for you on their hosted endpoints; the exposure works differently. +Client SDKs talk to the backend using keys that ship in your frontend code and are **public by +design**: the Firebase API key and Supabase publishable or legacy `anon` key. Client data access +needs Firebase Security Rules or Postgres row-level security (RLS) alongside grants, IAM, +endpoint authorization, and App Check where applicable. AI-generated apps repeatedly ship with +that layer open because "it worked in testing". On Supabase the gate also depends on how exposed +views and functions execute, not only on the table policies. -Components (recorded basis): +Documentation was checked in September 2026. Defaults below describe the documentation at the +time of writing; inspect the effective settings of each project. Live behavior has not been +demonstrated in this authoring environment. The Verify section records the missing prerequisites +and the vendor documentation supporting the REASONED checks. -- firebase: Firebase documentation (unknown) -- supabase: Supabase documentation (unknown) -- postgres: PostgreSQL documentation (unknown) -- postgres-view: PostgreSQL invoker-view minimum (15) -- curl: curl (curl-8_14_1) -- shellcheck: ShellCheck (v0.11.0) +## Firebase -Claims (full source mapping in the front matter): - -- public-config (REASONED; firebase, supabase): Firebase API keys/config and Supabase publishable/anon keys are public identifiers, not authorization; hosted endpoints handle TLS. -- firebase-ownership (REASONED; firebase): Require authentication and ownership separately in Firestore, RTDB and Storage; authentication-only rules do not isolate users. -- firebase-starting (REASONED; firebase): Firestore production and RTDB locked modes deny clients; test templates expose data for about a month. Inspect expiry and replace open rules. -- firestore-rules (REASONED; firebase): Rules version 2 recursive wildcards include the user document and descendants; version 1 excludes the parent. Unmatched paths deny and overlapping grants can reopen access. -- rtdb-rules (REASONED; firebase): Owner rules under users.$uid need ancestor grants removed; child rules cannot revoke parent grants or filter parent reads. -- firebase-storage (REASONED; firebase): Storage has separate rules; conflicting default prose/example warrants explicit deny-all followed by deliberate owner-path access. -- firebase-download (REASONED; firebase): Storage download URLs are non-expiring bearer credentials unaffected by tighter Rules; revoke tokens and use authorized getBlob/getBytes direct downloads. -- firebase-rule-deploy (REASONED; firebase): Inspect and test deployed rules for every database/bucket/instance; keep local and deployed definitions synchronized. -- firebase-server (REASONED; firebase): Admin SDK/server credentials bypass client Rules; keep credentials server-side and apply IAM and caller authorization. -- firebase-callable (REASONED; firebase): Callable ID-token validation is optional authentication; reject missing request.auth, use its UID and validate resource access. First-generation handlers use context.auth. -- firebase-callable-appcheck (REASONED; firebase): onCall enforceAppCheck needs firebase-functions 4.0.0 or newer; App Check does not replace caller/resource authorization. -- firebase-http (REASONED; firebase): onRequest does not inherit callable auth; verify end-user tokens with verifyIdToken or restrict IAM invokers, then authorize the intended principal. -- firebase-appcheck (REASONED; firebase): Register compatible clients and inspect metrics, then enforce App Check per product and per callable; registration alone enforces nothing. -- firebase-key-restrictions (REASONED; firebase): Apply one app-restriction type per key plus API restrictions; separate platforms and unrelated billable APIs, using the full Firebase product table. -- firebase-anonymous (REASONED; firebase): Anonymous sign-in is opt-in; disable unused providers because anonymous accounts still satisfy authentication-only Rules. -- firebase-enumeration (REASONED; firebase): Email enumeration protection defaults on only for projects created from September 15, 2023; signup can still return EMAIL_EXISTS. -- firebase-emulators (REASONED; firebase): Emulators default to localhost but missing Firestore/RTDB/Storage Rules configuration means open data; keep listeners, UI, hub, proxies and tunnels private. -- supabase-network (REASONED; supabase): Restrict hosted database/pooler client IPs with Network Restrictions, enforce SSL and protect database-role passwords. -- supabase-grants (REASONED; supabase): Data API access needs grants plus RLS; automatic-grant defaults are transitioning. Table Editor enables RLS, while SQL creation needs explicit enablement. -- supabase-select (REASONED; supabase, postgres): The profiles baseline enables RLS, revokes broad grants and permits authenticated owners SELECT; audit inherited/column grants and existing policies. -- supabase-bypass (REASONED; postgres): With RLS enabled and no policy, ordinary subject roles get no rows; owners normally bypass, as do superusers/BYPASSRLS roles. TRUNCATE is outside RLS. -- supabase-writes (REASONED; supabase, postgres): Grant and policy INSERT/UPDATE/DELETE only when intended; UPDATE checks old/new ownership and needs applicable SELECT when reading columns. -- supabase-schemas (REASONED; supabase): public is exposed by default; restrict exposed schemas and deliberately grant schema/object access. Schema selection does not replace RLS. -- supabase-api-disable (REASONED; supabase): Disable an unused Data API to close generated REST endpoints; this does not disable all other services. -- supabase-keys (REASONED; supabase): Publishable keys select anon or authenticated by session; secret and legacy service_role keys bypass RLS and belong only on trusted servers. -- supabase-key-retire (REASONED; supabase): Replacement keys do not disable legacy keys; migrate consumers then explicitly deactivate legacy keys, or delete compromised secret keys. -- supabase-jwt-retire (REASONED; supabase): Rotation changes the signing key but retains previous trust until revocation; account for verifier caches and propagation. -- supabase-view-owner (REASONED; postgres, supabase): Views use owner privileges/policies by default; disclosure depends on reachable schemas, grants, query and owner privileges, not anon-key possession alone. -- supabase-view-invoker (REASONED; postgres-view, postgres): PostgreSQL 15+ security_invoker views apply caller policies and require underlying privileges. -- supabase-view-legacy (REASONED; postgres): PostgreSQL 14 and earlier require revoking all view privileges from PUBLIC/anon/authenticated or unexposing the schema; updatable views can bypass RLS for writes. -- supabase-functions (REASONED; supabase, postgres): Prefer INVOKER; restrict DEFINER execution, use empty search_path with qualified references and enforce caller authorization including required MFA. -- supabase-function-defaults (REASONED; postgres): New functions grant PUBLIC execution; create privileged functions and restrict grants transactionally. Global/default revocations are creator-specific and do not fix existing functions. -- supabase-http (REASONED; supabase): Audit http/pg_net wrappers and revoke unneeded execute grants; constrain callers/destinations/data. Extension enablement alone does not prove exploitable SSRF. -- supabase-vault (REASONED; supabase): Restrict vault.decrypted_secrets and privileged wrappers and keep Vault unexposed; at-rest encryption cannot prevent authorized plaintext queries. -- supabase-buckets (REASONED; supabase): Private downloads need authorized access or time-limited signed URLs; public buckets bypass retrieval checks but not upload/delete/move/copy controls. -- supabase-storage-policies (REASONED; supabase): Authorize storage.objects separately by bucket and owner path; upload needs INSERT, upsert also SELECT/UPDATE, and deletion needs deliberate permission. -- supabase-realtime-default (REASONED; supabase): Public Realtime channels are allowed by default; disable them project-wide and also set client config.private. -- supabase-realtime-policy (REASONED; supabase): Broadcast/Presence receive/send need separate SELECT/INSERT policies on realtime.messages, constrained by topic ownership and extension. -- supabase-changes (REASONED; supabase): Postgres Changes uses source-table authorization; private-channel policies are not universal and DELETE has separate limitations. -- supabase-signup (REASONED; supabase): Disable signup, anonymous sign-ins and unused providers when unwanted; inspect hosted settings instead of assuming universal defaults. -- supabase-cli-auth (REASONED; supabase): CLI enable_signup defaults true and enable_anonymous_sign_ins false; local config edits do not prove hosted settings changed. -- supabase-mfa (REASONED; supabase, postgres): TOTP is available on every plan at writing; enrollment alone does not constrain data. Require aal2 using a restrictive policy alongside permissive ownership policies. -- supabase-edge-jwt (REASONED; supabase): Per-function verify_jwt defaults true; --no-verify-jwt disables platform checking and requires handler authentication before protected work. -- supabase-edge-auth (REASONED; supabase): Platform JWT success is not resource authorization and API-key compatibility can pass it; separately authorize users or verify webhook signatures. -- supabase-edge-secrets (REASONED; supabase): Edge environments contain privileged credentials including SUPABASE_SERVICE_ROLE_KEY; protect managed/local secrets, responses, logs and client bundles. -- verify-firestore (REASONED; firebase): Compare unsigned/A/B reads and writes to user documents and descendants, with a successful owner control; test parent documents to distinguish recursive-rule versions. -- verify-rtdb (REASONED; firebase): Compare unsigned/A/B reads/writes to seeded RTDB owner paths; ancestor grants expose them while ownership rules deny unauthorized callers. -- verify-firebase-storage (REASONED; firebase): Compare unsigned/A/B object reads and uploads; ownership rules must deny unrelated callers while owner operations succeed. -- verify-download-token (REASONED; firebase): Same sessionless download URL works after Rules tightening but must lose bytes after token revocation; owner getBytes remains successful and network/CORS failures are inconclusive. -- verify-tables (REASONED; supabase, postgres, curl): Pair unsigned/A/B profiles GETs against seeded rows, using ordinary credentials and required MFA; test intended writes and persisted effects separately. -- verify-views (REASONED; postgres): Compare view reads with table boundaries; owner-privileged exposure must close under invoker/revoked access while intended reads or deliberate removal are confirmed. -- verify-rpc (REASONED; supabase): Test every API function overload with valid harmless arguments as unsigned/A/B; unauthorized data or operations must be denied while the intended caller succeeds. -- verify-http (REASONED; supabase): Probe HTTP wrappers with a controlled collector and marker; unauthorized fixed calls must emit no request. Commit pg_net fixture transactions before expecting delivery. -- verify-vault (REASONED; supabase, postgres): Use a disposable Vault canary to compare exposed/fixed wrappers; inspect function and table privileges as supporting evidence. -- verify-storage (REASONED; supabase): Compare public URL and A/B private downloads plus intended uploads/upserts; unauthorized callers must receive no bytes or cross-owner writes. -- verify-realtime (REASONED; supabase): Compare A/B join, Broadcast and Presence delivery; public-disabled clients lacking private=true must fail, and connection alone is insufficient. -- verify-changes (REASONED; supabase): Observe A/B INSERT/UPDATE subscription events against seeded rows; fixed table authorization withholds unrelated events. Evaluate DELETE separately. -- verify-edge (REASONED; supabase): Compare no/invalid Bearer without apikey, authorized owners and valid webhook signatures; distinguish platform 401 from handler authorization and inspect effects/secrets. -- verify-firebase-functions (REASONED; firebase): Compare callable requests with valid App Check but missing user auth and A/B operations; HTTP endpoints need separate invalid/valid user or IAM comparisons. -- verify-appcheck (REASONED; firebase): Repeat owner requests with/without App Check per product/callable; enforcement must deny missing attestation while compatible clients work. -- verify-api-restrictions (REASONED; firebase): Test intended versus excluded apps/referrers after key restrictions, retaining successful intended product traffic. -- verify-emulators (REASONED; firebase): Enumerate hub /emulators and sockets, compare missing versus loaded Rules and second-host connectivity, and retain local owner controls. -- verify-firebase-accounts (REASONED; firebase): Sign out before each Anonymous signup test and check isNewUser; disabled providers must reject creation. Enumeration tests compare sign-in errors, not EMAIL_EXISTS signup behavior. -- verify-supabase-accounts (REASONED; supabase): Fresh signup and anonymous creation must fail under invite-only/anonymous-disabled settings while an invited account still signs in. -- verify-mfa (REASONED; supabase): Owner profiles GET with aal1 must yield no protected rows under the restrictive policy, while fresh aal2 returns the seeded owner row. -- verify-key-retirement (REASONED; supabase): Compare old/new API keys after explicit deactivation and old/new JWTs after revocation, accounting for propagation and caches. -- verify-api-removal (REASONED; supabase): Previously successful profiles requests must stop returning fixtures after schema removal or Data API disabling; retained schemas need authorized controls. -- verify-bundle (REASONED; supabase): Filename-only service_role/eyJ/sb_secret_ scanning yields review candidates, not proof of leaks; public anon JWTs are legitimate. Use a marker control and inspect encoded/split secrets separately. -- local-guards (DEMONSTRATED; supabase, shellcheck): Historical local guard tests rejected placeholders and malformed set lines; this guide's assessment is shell behavior only, not live authorization. The recorded ShellCheck 0.11.0 has a release citation, not host-binary evidence; bash and other tool/parser versions remain unrecorded. -- local-syntax (DEMONSTRATED; firebase, supabase, shellcheck): Recorded ShellCheck 0.11.0/bash and JSON/TOML/JavaScript parsing passed; this guide's assessment is that these checks do not compile Firebase Rules, execute SQL or demonstrate services. Only the ShellCheck release is cited; bash and other tool/parser versions remain unrecorded. - - -These platforms handle TLS for you on their hosted endpoints; the exposure works differently. -Client SDKs talk to the backend using keys that ship in your frontend code and are **public by -design**: the Firebase API key and Supabase publishable or legacy `anon` key. Client data access -needs Firebase Security Rules or Postgres row-level security (RLS) alongside grants, IAM, -endpoint authorization, and App Check where applicable. AI-generated apps repeatedly ship with -that layer open because "it worked in testing". On Supabase the gate also depends on how exposed -views and functions execute, not only on the table policies. - -Documentation was checked in September 2026. Defaults below describe the documentation at the -time of writing; inspect the effective settings of each project. Live behavior has not been -demonstrated in this authoring environment. The Verify section records the missing prerequisites -and the vendor documentation supporting the REASONED checks. - -## Firebase - -### 1. Set ownership rules in all three products +### 1. Set ownership rules in all three products Every Firestore, Realtime Database, and Storage instance needs explicit security rules. Never deploy the all-open rule (`allow read, write: if true;` or `".read": true, ".write": true`); @@ -1235,3 +952,312 @@ REASONED: tool-version provenance for the historical local runs above; the [Shel - [curl curl-8_14_1 write-out fields and version requirements (checked October 2026)](https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79). - [curl curl-8_14_1 headers from stdin (checked October 2026)](https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md). - [ShellCheck v0.11.0 changelog release entry (checked October 2026)](https://github.com/koalaman/shellcheck/blob/v0.11.0/CHANGELOG.md#L1). + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/firebase-supabase.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- firebase: Firebase documentation (unknown) +- supabase: Supabase documentation (unknown) +- postgres: PostgreSQL documentation (unknown) +- postgres-view: PostgreSQL invoker-view minimum (15) +- curl: curl (curl-8_14_1) +- shellcheck: ShellCheck (v0.11.0) + +Claims (full source mapping in the JSON block below): + +- public-config (REASONED; firebase, supabase): Firebase API keys/config and Supabase publishable/anon keys are public identifiers, not authorization; hosted endpoints handle TLS. +- firebase-ownership (REASONED; firebase): Require authentication and ownership separately in Firestore, RTDB and Storage; authentication-only rules do not isolate users. +- firebase-starting (REASONED; firebase): Firestore production and RTDB locked modes deny clients; test templates expose data for about a month. Inspect expiry and replace open rules. +- firestore-rules (REASONED; firebase): Rules version 2 recursive wildcards include the user document and descendants; version 1 excludes the parent. Unmatched paths deny and overlapping grants can reopen access. +- rtdb-rules (REASONED; firebase): Owner rules under users.$uid need ancestor grants removed; child rules cannot revoke parent grants or filter parent reads. +- firebase-storage (REASONED; firebase): Storage has separate rules; conflicting default prose/example warrants explicit deny-all followed by deliberate owner-path access. +- firebase-download (REASONED; firebase): Storage download URLs are non-expiring bearer credentials unaffected by tighter Rules; revoke tokens and use authorized getBlob/getBytes direct downloads. +- firebase-rule-deploy (REASONED; firebase): Inspect and test deployed rules for every database/bucket/instance; keep local and deployed definitions synchronized. +- firebase-server (REASONED; firebase): Admin SDK/server credentials bypass client Rules; keep credentials server-side and apply IAM and caller authorization. +- firebase-callable (REASONED; firebase): Callable ID-token validation is optional authentication; reject missing request.auth, use its UID and validate resource access. First-generation handlers use context.auth. +- firebase-callable-appcheck (REASONED; firebase): onCall enforceAppCheck needs firebase-functions 4.0.0 or newer; App Check does not replace caller/resource authorization. +- firebase-http (REASONED; firebase): onRequest does not inherit callable auth; verify end-user tokens with verifyIdToken or restrict IAM invokers, then authorize the intended principal. +- firebase-appcheck (REASONED; firebase): Register compatible clients and inspect metrics, then enforce App Check per product and per callable; registration alone enforces nothing. +- firebase-key-restrictions (REASONED; firebase): Apply one app-restriction type per key plus API restrictions; separate platforms and unrelated billable APIs, using the full Firebase product table. +- firebase-anonymous (REASONED; firebase): Anonymous sign-in is opt-in; disable unused providers because anonymous accounts still satisfy authentication-only Rules. +- firebase-enumeration (REASONED; firebase): Email enumeration protection defaults on only for projects created from September 15, 2023; signup can still return EMAIL_EXISTS. +- firebase-emulators (REASONED; firebase): Emulators default to localhost but missing Firestore/RTDB/Storage Rules configuration means open data; keep listeners, UI, hub, proxies and tunnels private. +- supabase-network (REASONED; supabase): Restrict hosted database/pooler client IPs with Network Restrictions, enforce SSL and protect database-role passwords. +- supabase-grants (REASONED; supabase): Data API access needs grants plus RLS; automatic-grant defaults are transitioning. Table Editor enables RLS, while SQL creation needs explicit enablement. +- supabase-select (REASONED; supabase, postgres): The profiles baseline enables RLS, revokes broad grants and permits authenticated owners SELECT; audit inherited/column grants and existing policies. +- supabase-bypass (REASONED; postgres): With RLS enabled and no policy, ordinary subject roles get no rows; owners normally bypass, as do superusers/BYPASSRLS roles. TRUNCATE is outside RLS. +- supabase-writes (REASONED; supabase, postgres): Grant and policy INSERT/UPDATE/DELETE only when intended; UPDATE checks old/new ownership and needs applicable SELECT when reading columns. +- supabase-schemas (REASONED; supabase): public is exposed by default; restrict exposed schemas and deliberately grant schema/object access. Schema selection does not replace RLS. +- supabase-api-disable (REASONED; supabase): Disable an unused Data API to close generated REST endpoints; this does not disable all other services. +- supabase-keys (REASONED; supabase): Publishable keys select anon or authenticated by session; secret and legacy service_role keys bypass RLS and belong only on trusted servers. +- supabase-key-retire (REASONED; supabase): Replacement keys do not disable legacy keys; migrate consumers then explicitly deactivate legacy keys, or delete compromised secret keys. +- supabase-jwt-retire (REASONED; supabase): Rotation changes the signing key but retains previous trust until revocation; account for verifier caches and propagation. +- supabase-view-owner (REASONED; postgres, supabase): Views use owner privileges/policies by default; disclosure depends on reachable schemas, grants, query and owner privileges, not anon-key possession alone. +- supabase-view-invoker (REASONED; postgres-view, postgres): PostgreSQL 15+ security_invoker views apply caller policies and require underlying privileges. +- supabase-view-legacy (REASONED; postgres): PostgreSQL 14 and earlier require revoking all view privileges from PUBLIC/anon/authenticated or unexposing the schema; updatable views can bypass RLS for writes. +- supabase-functions (REASONED; supabase, postgres): Prefer INVOKER; restrict DEFINER execution, use empty search_path with qualified references and enforce caller authorization including required MFA. +- supabase-function-defaults (REASONED; postgres): New functions grant PUBLIC execution; create privileged functions and restrict grants transactionally. Global/default revocations are creator-specific and do not fix existing functions. +- supabase-http (REASONED; supabase): Audit http/pg_net wrappers and revoke unneeded execute grants; constrain callers/destinations/data. Extension enablement alone does not prove exploitable SSRF. +- supabase-vault (REASONED; supabase): Restrict vault.decrypted_secrets and privileged wrappers and keep Vault unexposed; at-rest encryption cannot prevent authorized plaintext queries. +- supabase-buckets (REASONED; supabase): Private downloads need authorized access or time-limited signed URLs; public buckets bypass retrieval checks but not upload/delete/move/copy controls. +- supabase-storage-policies (REASONED; supabase): Authorize storage.objects separately by bucket and owner path; upload needs INSERT, upsert also SELECT/UPDATE, and deletion needs deliberate permission. +- supabase-realtime-default (REASONED; supabase): Public Realtime channels are allowed by default; disable them project-wide and also set client config.private. +- supabase-realtime-policy (REASONED; supabase): Broadcast/Presence receive/send need separate SELECT/INSERT policies on realtime.messages, constrained by topic ownership and extension. +- supabase-changes (REASONED; supabase): Postgres Changes uses source-table authorization; private-channel policies are not universal and DELETE has separate limitations. +- supabase-signup (REASONED; supabase): Disable signup, anonymous sign-ins and unused providers when unwanted; inspect hosted settings instead of assuming universal defaults. +- supabase-cli-auth (REASONED; supabase): CLI enable_signup defaults true and enable_anonymous_sign_ins false; local config edits do not prove hosted settings changed. +- supabase-mfa (REASONED; supabase, postgres): TOTP is available on every plan at writing; enrollment alone does not constrain data. Require aal2 using a restrictive policy alongside permissive ownership policies. +- supabase-edge-jwt (REASONED; supabase): Per-function verify_jwt defaults true; --no-verify-jwt disables platform checking and requires handler authentication before protected work. +- supabase-edge-auth (REASONED; supabase): Platform JWT success is not resource authorization and API-key compatibility can pass it; separately authorize users or verify webhook signatures. +- supabase-edge-secrets (REASONED; supabase): Edge environments contain privileged credentials including SUPABASE_SERVICE_ROLE_KEY; protect managed/local secrets, responses, logs and client bundles. +- verify-firestore (REASONED; firebase): Compare unsigned/A/B reads and writes to user documents and descendants, with a successful owner control; test parent documents to distinguish recursive-rule versions. +- verify-rtdb (REASONED; firebase): Compare unsigned/A/B reads/writes to seeded RTDB owner paths; ancestor grants expose them while ownership rules deny unauthorized callers. +- verify-firebase-storage (REASONED; firebase): Compare unsigned/A/B object reads and uploads; ownership rules must deny unrelated callers while owner operations succeed. +- verify-download-token (REASONED; firebase): Same sessionless download URL works after Rules tightening but must lose bytes after token revocation; owner getBytes remains successful and network/CORS failures are inconclusive. +- verify-tables (REASONED; supabase, postgres, curl): Pair unsigned/A/B profiles GETs against seeded rows, using ordinary credentials and required MFA; test intended writes and persisted effects separately. +- verify-views (REASONED; postgres): Compare view reads with table boundaries; owner-privileged exposure must close under invoker/revoked access while intended reads or deliberate removal are confirmed. +- verify-rpc (REASONED; supabase): Test every API function overload with valid harmless arguments as unsigned/A/B; unauthorized data or operations must be denied while the intended caller succeeds. +- verify-http (REASONED; supabase): Probe HTTP wrappers with a controlled collector and marker; unauthorized fixed calls must emit no request. Commit pg_net fixture transactions before expecting delivery. +- verify-vault (REASONED; supabase, postgres): Use a disposable Vault canary to compare exposed/fixed wrappers; inspect function and table privileges as supporting evidence. +- verify-storage (REASONED; supabase): Compare public URL and A/B private downloads plus intended uploads/upserts; unauthorized callers must receive no bytes or cross-owner writes. +- verify-realtime (REASONED; supabase): Compare A/B join, Broadcast and Presence delivery; public-disabled clients lacking private=true must fail, and connection alone is insufficient. +- verify-changes (REASONED; supabase): Observe A/B INSERT/UPDATE subscription events against seeded rows; fixed table authorization withholds unrelated events. Evaluate DELETE separately. +- verify-edge (REASONED; supabase): Compare no/invalid Bearer without apikey, authorized owners and valid webhook signatures; distinguish platform 401 from handler authorization and inspect effects/secrets. +- verify-firebase-functions (REASONED; firebase): Compare callable requests with valid App Check but missing user auth and A/B operations; HTTP endpoints need separate invalid/valid user or IAM comparisons. +- verify-appcheck (REASONED; firebase): Repeat owner requests with/without App Check per product/callable; enforcement must deny missing attestation while compatible clients work. +- verify-api-restrictions (REASONED; firebase): Test intended versus excluded apps/referrers after key restrictions, retaining successful intended product traffic. +- verify-emulators (REASONED; firebase): Enumerate hub /emulators and sockets, compare missing versus loaded Rules and second-host connectivity, and retain local owner controls. +- verify-firebase-accounts (REASONED; firebase): Sign out before each Anonymous signup test and check isNewUser; disabled providers must reject creation. Enumeration tests compare sign-in errors, not EMAIL_EXISTS signup behavior. +- verify-supabase-accounts (REASONED; supabase): Fresh signup and anonymous creation must fail under invite-only/anonymous-disabled settings while an invited account still signs in. +- verify-mfa (REASONED; supabase): Owner profiles GET with aal1 must yield no protected rows under the restrictive policy, while fresh aal2 returns the seeded owner row. +- verify-key-retirement (REASONED; supabase): Compare old/new API keys after explicit deactivation and old/new JWTs after revocation, accounting for propagation and caches. +- verify-api-removal (REASONED; supabase): Previously successful profiles requests must stop returning fixtures after schema removal or Data API disabling; retained schemas need authorized controls. +- verify-bundle (REASONED; supabase): Filename-only service_role/eyJ/sb_secret_ scanning yields review candidates, not proof of leaks; public anon JWTs are legitimate. Use a marker control and inspect encoded/split secrets separately. +- local-guards (DEMONSTRATED; supabase, shellcheck): Historical local guard tests rejected placeholders and malformed set lines; this guide's assessment is shell behavior only, not live authorization. The recorded ShellCheck 0.11.0 has a release citation, not host-binary evidence; bash and other tool/parser versions remain unrecorded. +- local-syntax (DEMONSTRATED; firebase, supabase, shellcheck): Recorded ShellCheck 0.11.0/bash and JSON/TOML/JavaScript parsing passed; this guide's assessment is that these checks do not compile Firebase Rules, execute SQL or demonstrate services. Only the ShellCheck release is cited; bash and other tool/parser versions remain unrecorded. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "6690687db9e1d66f3e67a898fc0b030022e55be24166bc29b8c35554e8081891", + "components": { + "firebase": { + "name": "Firebase documentation", + "basis": "unknown", + "sources": { + "s100c2cdbdd59": "https://firebase.google.com/docs/rules", + "s53bfe8b21868": "https://firebase.google.com/docs/rules/basics", + "s622d3bb0665b": "https://firebase.google.com/docs/firestore/quickstart", + "sb2c933c841ef": "https://firebase.google.com/docs/database/web/start", + "s75f018e0fac7": "https://firebase.google.com/support/releases", + "s7bd11c22c7ce": "https://firebase.google.com/docs/firestore/security/rules-structure", + "sa6b1c4933e6a": "https://firebase.google.com/docs/database/security/core-syntax", + "s0a7f54463abd": "https://firebase.google.com/docs/storage/security/core-syntax", + "sd6071548f6fe": "https://firebase.google.com/docs/firestore/security/get-started", + "s14872143a9e8": "https://firebase.google.com/docs/storage/security/get-started", + "sc8f214d059d2": "https://firebase.google.com/docs/functions/callable", + "s79da8f65f90a": "https://firebase.google.com/docs/functions/callable-reference", + "s9a86df9623a6": "https://firebase.google.com/docs/functions/1st-gen/callable-1st", + "s49f78d054708": "https://firebase.google.com/docs/auth/admin/verify-id-tokens", + "s1bb711c9afeb": "https://firebase.google.com/docs/functions/http-events", + "sc9f86ea271f6": "https://firebase.google.com/docs/reference/functions/2nd-gen/node/firebase-functions.https.httpsoptions", + "sb0ff8f1726f7": "https://firebase.google.com/docs/app-check/cloud-functions", + "sa71e958c99df": "https://firebase.google.com/docs/app-check/web/recaptcha-enterprise-provider", + "sba7129c83405": "https://firebase.google.com/docs/app-check/enable-enforcement", + "safeed4cd2b67": "https://firebase.google.com/docs/projects/api-keys", + "sc78d5eb59b65": "https://docs.cloud.google.com/api-keys/docs/add-restrictions-api-keys", + "sb0d26775f3b4": "https://firebase.google.com/docs/web/learn-more", + "s77e2cbd8a29a": "https://firebase.google.com/docs/auth/web/anonymous-auth", + "s06808e4b6965": "https://docs.cloud.google.com/identity-platform/docs/admin/email-enumeration-protection", + "sd8a1bda29f5c": "https://firebase.google.com/docs/emulator-suite/install_and_configure", + "s69d9721c7f29": "https://firebase.google.com/docs/emulator-suite/use_hosting", + "s8c90f9f6c7fa": "https://firebase.google.com/docs/reference/js/auth", + "s8c8336ec252e": "https://firebase.google.com/docs/reference/js/auth.additionaluserinfo", + "s142e56472093": "https://firebase.google.com/docs/auth/admin/manage-sessions", + "s9b7e57b472c7": "https://firebase.google.com/docs/storage/admin/start", + "sa0584b797732": "https://firebase.google.com/docs/reference/kotlin/com/google/firebase/storage/StorageReference", + "sfd1e7412abf6": "https://firebase.google.com/docs/storage/web/download-files" + } + }, + "supabase": { + "name": "Supabase documentation", + "basis": "unknown", + "sources": { + "s73d939b4644e": "https://supabase.com/docs/guides/api/api-keys", + "s46cff2590323": "https://supabase.com/docs/guides/database/postgres/row-level-security", + "s741468ff35a9": "https://supabase.com/docs/guides/auth/auth-mfa", + "s6d5cf2d4dbf3": "https://supabase.com/docs/guides/api/securing-your-api", + "s20ef046ac646": "https://supabase.com/docs/guides/api/using-custom-schemas", + "sa946354db383": "https://supabase.com/docs/guides/getting-started/migrating-to-new-api-keys", + "sfc2f9ae0232b": "https://supabase.com/docs/guides/auth/signing-keys", + "s8405a9fb4b74": "https://supabase.com/docs/guides/database/functions", + "s13da01bbbba5": "https://supabase.com/docs/guides/database/extensions/http", + "s76740bffcb32": "https://supabase.com/docs/guides/database/extensions/pg_net", + "s47bb8fa3e14e": "https://supabase.com/docs/guides/database/vault", + "s1567fb838f20": "https://supabase.com/docs/guides/storage/buckets/fundamentals", + "sdba7d7b6dcf8": "https://supabase.com/docs/guides/storage/security/access-control", + "s9e1484f46eff": "https://supabase.com/docs/guides/storage/schema/helper-functions", + "sc29febbb230e": "https://supabase.com/docs/guides/storage/serving/downloads", + "see2d3be99ebd": "https://supabase.com/docs/guides/storage/uploads/standard-uploads", + "s5674b1057e14": "https://supabase.com/docs/guides/realtime/settings", + "s84efad0ba39d": "https://supabase.com/docs/guides/realtime/authorization", + "s694eab64f2ec": "https://supabase.com/docs/guides/realtime/postgres-changes", + "sc797f0b63472": "https://supabase.com/docs/guides/auth/general-configuration", + "s3a5e39fe7b4d": "https://supabase.com/docs/guides/local-development/cli/config", + "sa2dd4637969e": "https://supabase.com/docs/guides/auth/passwords", + "sad79d4d1b6ee": "https://supabase.com/docs/guides/auth/auth-anonymous", + "s182755725d74": "https://supabase.com/docs/guides/auth/auth-mfa/totp", + "s92b405dc3c7e": "https://supabase.com/docs/reference/javascript/select", + "s087c8cf42ab0": "https://supabase.com/docs/reference/javascript/rpc", + "sf3246532dd18": "https://supabase.com/docs/guides/functions", + "s7dfbb47b5d20": "https://supabase.com/docs/reference/cli/supabase-functions-deploy", + "sf7d438b8c080": "https://supabase.com/docs/guides/functions/auth", + "se52bbd097033": "https://supabase.com/docs/guides/functions/auth-headers", + "sb3c4c0993e14": "https://supabase.com/docs/guides/functions/secrets", + "sb787b5d63633": "https://supabase.com/docs/guides/platform/network-restrictions", + "s83e15676e1ff": "https://supabase.com/docs/guides/platform/ssl-enforcement", + "sf69cafb4bc6c": "https://supabase.com/docs/guides/database/postgres/roles" + } + }, + "postgres": { + "name": "PostgreSQL documentation", + "basis": "unknown", + "sources": { + "s13c54ab9cc6b": "https://www.postgresql.org/docs/current/sql-createview.html", + "saa99ef28f60f": "https://www.postgresql.org/docs/current/sql-alterview.html", + "sb6224f4ac866": "https://www.postgresql.org/docs/current/sql-revoke.html", + "se9846041ce1f": "https://www.postgresql.org/docs/current/ddl-rowsecurity.html", + "s8e65a2f3f2bb": "https://www.postgresql.org/docs/current/sql-createpolicy.html", + "sec39c317b06f": "https://www.postgresql.org/docs/current/sql-createfunction.html", + "s0fe66d71aa10": "https://www.postgresql.org/docs/current/sql-alterfunction.html", + "s04e81888353f": "https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html", + "sc39eceeac711": "https://www.postgresql.org/docs/current/functions-info.html" + } + }, + "postgres-view": { + "name": "PostgreSQL invoker-view minimum", + "basis": "15", + "sources": { + "s14443e063ef1": "https://www.postgresql.org/docs/release/15.0/" + } + }, + "curl": { + "name": "curl", + "basis": "curl-8_14_1", + "sources": { + "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", + "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md" + } + }, + "shellcheck": { + "name": "ShellCheck", + "basis": "v0.11.0", + "sources": { + "sf994764bf270": "https://github.com/koalaman/shellcheck/blob/v0.11.0/CHANGELOG.md#L1" + } + } + }, + "claims": { + "public-config": {"text": "Firebase API keys/config and Supabase publishable/anon keys are public identifiers, not authorization; hosted endpoints handle TLS.", "components": ["firebase", "supabase"], "sources": ["firebase:safeed4cd2b67", "firebase:sb0d26775f3b4", "supabase:s73d939b4644e"], "status": "REASONED"}, + "firebase-ownership": {"text": "Require authentication and ownership separately in Firestore, RTDB and Storage; authentication-only rules do not isolate users.", "components": ["firebase"], "sources": ["firebase:s100c2cdbdd59", "firebase:s53bfe8b21868"], "status": "REASONED"}, + "firebase-starting": {"text": "Firestore production and RTDB locked modes deny clients; test templates expose data for about a month. Inspect expiry and replace open rules.", "components": ["firebase"], "sources": ["firebase:s622d3bb0665b", "firebase:sb2c933c841ef", "firebase:s75f018e0fac7"], "status": "REASONED"}, + "firestore-rules": {"text": "Rules version 2 recursive wildcards include the user document and descendants; version 1 excludes the parent. Unmatched paths deny and overlapping grants can reopen access.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, + "rtdb-rules": {"text": "Owner rules under users.$uid need ancestor grants removed; child rules cannot revoke parent grants or filter parent reads.", "components": ["firebase"], "sources": ["firebase:sa6b1c4933e6a"], "status": "REASONED"}, + "firebase-storage": {"text": "Storage has separate rules; conflicting default prose/example warrants explicit deny-all followed by deliberate owner-path access.", "components": ["firebase"], "sources": ["firebase:s53bfe8b21868", "firebase:s0a7f54463abd"], "status": "REASONED"}, + "firebase-download": {"text": "Storage download URLs are non-expiring bearer credentials unaffected by tighter Rules; revoke tokens and use authorized getBlob/getBytes direct downloads.", "components": ["firebase"], "sources": ["firebase:s9b7e57b472c7", "firebase:sa0584b797732", "firebase:sfd1e7412abf6"], "status": "REASONED"}, + "firebase-rule-deploy": {"text": "Inspect and test deployed rules for every database/bucket/instance; keep local and deployed definitions synchronized.", "components": ["firebase"], "sources": ["firebase:sd6071548f6fe", "firebase:s14872143a9e8"], "status": "REASONED"}, + "firebase-server": {"text": "Admin SDK/server credentials bypass client Rules; keep credentials server-side and apply IAM and caller authorization.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, + "firebase-callable": {"text": "Callable ID-token validation is optional authentication; reject missing request.auth, use its UID and validate resource access. First-generation handlers use context.auth.", "components": ["firebase"], "sources": ["firebase:s79da8f65f90a", "firebase:sc8f214d059d2", "firebase:s9a86df9623a6"], "status": "REASONED"}, + "firebase-callable-appcheck": {"text": "onCall enforceAppCheck needs firebase-functions 4.0.0 or newer; App Check does not replace caller/resource authorization.", "components": ["firebase"], "sources": ["firebase:sb0ff8f1726f7"], "status": "REASONED"}, + "firebase-http": {"text": "onRequest does not inherit callable auth; verify end-user tokens with verifyIdToken or restrict IAM invokers, then authorize the intended principal.", "components": ["firebase"], "sources": ["firebase:s49f78d054708", "firebase:s1bb711c9afeb", "firebase:sc9f86ea271f6"], "status": "REASONED"}, + "firebase-appcheck": {"text": "Register compatible clients and inspect metrics, then enforce App Check per product and per callable; registration alone enforces nothing.", "components": ["firebase"], "sources": ["firebase:sa71e958c99df", "firebase:sba7129c83405", "firebase:sb0ff8f1726f7"], "status": "REASONED"}, + "firebase-key-restrictions": {"text": "Apply one app-restriction type per key plus API restrictions; separate platforms and unrelated billable APIs, using the full Firebase product table.", "components": ["firebase"], "sources": ["firebase:sc78d5eb59b65", "firebase:safeed4cd2b67"], "status": "REASONED"}, + "firebase-anonymous": {"text": "Anonymous sign-in is opt-in; disable unused providers because anonymous accounts still satisfy authentication-only Rules.", "components": ["firebase"], "sources": ["firebase:s77e2cbd8a29a"], "status": "REASONED"}, + "firebase-enumeration": {"text": "Email enumeration protection defaults on only for projects created from September 15, 2023; signup can still return EMAIL_EXISTS.", "components": ["firebase"], "sources": ["firebase:s06808e4b6965"], "status": "REASONED"}, + "firebase-emulators": {"text": "Emulators default to localhost but missing Firestore/RTDB/Storage Rules configuration means open data; keep listeners, UI, hub, proxies and tunnels private.", "components": ["firebase"], "sources": ["firebase:sd8a1bda29f5c", "firebase:s69d9721c7f29"], "status": "REASONED"}, + "supabase-network": {"text": "Restrict hosted database/pooler client IPs with Network Restrictions, enforce SSL and protect database-role passwords.", "components": ["supabase"], "sources": ["supabase:sb787b5d63633", "supabase:s83e15676e1ff", "supabase:sf69cafb4bc6c"], "status": "REASONED"}, + "supabase-grants": {"text": "Data API access needs grants plus RLS; automatic-grant defaults are transitioning. Table Editor enables RLS, while SQL creation needs explicit enablement.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3"], "status": "REASONED"}, + "supabase-select": {"text": "The profiles baseline enables RLS, revokes broad grants and permits authenticated owners SELECT; audit inherited/column grants and existing policies.", "components": ["supabase", "postgres"], "sources": ["supabase:s46cff2590323", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, + "supabase-bypass": {"text": "With RLS enabled and no policy, ordinary subject roles get no rows; owners normally bypass, as do superusers/BYPASSRLS roles. TRUNCATE is outside RLS.", "components": ["postgres"], "sources": ["postgres:se9846041ce1f"], "status": "REASONED"}, + "supabase-writes": {"text": "Grant and policy INSERT/UPDATE/DELETE only when intended; UPDATE checks old/new ownership and needs applicable SELECT when reading columns.", "components": ["supabase", "postgres"], "sources": ["supabase:s46cff2590323", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, + "supabase-schemas": {"text": "public is exposed by default; restrict exposed schemas and deliberately grant schema/object access. Schema selection does not replace RLS.", "components": ["supabase"], "sources": ["supabase:s20ef046ac646"], "status": "REASONED"}, + "supabase-api-disable": {"text": "Disable an unused Data API to close generated REST endpoints; this does not disable all other services.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3"], "status": "REASONED"}, + "supabase-keys": {"text": "Publishable keys select anon or authenticated by session; secret and legacy service_role keys bypass RLS and belong only on trusted servers.", "components": ["supabase"], "sources": ["supabase:s73d939b4644e"], "status": "REASONED"}, + "supabase-key-retire": {"text": "Replacement keys do not disable legacy keys; migrate consumers then explicitly deactivate legacy keys, or delete compromised secret keys.", "components": ["supabase"], "sources": ["supabase:sa946354db383"], "status": "REASONED"}, + "supabase-jwt-retire": {"text": "Rotation changes the signing key but retains previous trust until revocation; account for verifier caches and propagation.", "components": ["supabase"], "sources": ["supabase:sfc2f9ae0232b"], "status": "REASONED"}, + "supabase-view-owner": {"text": "Views use owner privileges/policies by default; disclosure depends on reachable schemas, grants, query and owner privileges, not anon-key possession alone.", "components": ["postgres", "supabase"], "sources": ["postgres:s13c54ab9cc6b", "supabase:s46cff2590323"], "status": "REASONED"}, + "supabase-view-invoker": {"text": "PostgreSQL 15+ security_invoker views apply caller policies and require underlying privileges.", "components": ["postgres-view", "postgres"], "sources": ["postgres-view:s14443e063ef1", "postgres:saa99ef28f60f", "postgres:s13c54ab9cc6b"], "status": "REASONED"}, + "supabase-view-legacy": {"text": "PostgreSQL 14 and earlier require revoking all view privileges from PUBLIC/anon/authenticated or unexposing the schema; updatable views can bypass RLS for writes.", "components": ["postgres"], "sources": ["postgres:s13c54ab9cc6b", "postgres:sb6224f4ac866", "postgres:sc39eceeac711"], "status": "REASONED"}, + "supabase-functions": {"text": "Prefer INVOKER; restrict DEFINER execution, use empty search_path with qualified references and enforce caller authorization including required MFA.", "components": ["supabase", "postgres"], "sources": ["supabase:s8405a9fb4b74", "postgres:sec39c317b06f", "postgres:s0fe66d71aa10"], "status": "REASONED"}, + "supabase-function-defaults": {"text": "New functions grant PUBLIC execution; create privileged functions and restrict grants transactionally. Global/default revocations are creator-specific and do not fix existing functions.", "components": ["postgres"], "sources": ["postgres:sec39c317b06f", "postgres:s04e81888353f"], "status": "REASONED"}, + "supabase-http": {"text": "Audit http/pg_net wrappers and revoke unneeded execute grants; constrain callers/destinations/data. Extension enablement alone does not prove exploitable SSRF.", "components": ["supabase"], "sources": ["supabase:s13da01bbbba5", "supabase:s76740bffcb32"], "status": "REASONED"}, + "supabase-vault": {"text": "Restrict vault.decrypted_secrets and privileged wrappers and keep Vault unexposed; at-rest encryption cannot prevent authorized plaintext queries.", "components": ["supabase"], "sources": ["supabase:s47bb8fa3e14e"], "status": "REASONED"}, + "supabase-buckets": {"text": "Private downloads need authorized access or time-limited signed URLs; public buckets bypass retrieval checks but not upload/delete/move/copy controls.", "components": ["supabase"], "sources": ["supabase:s1567fb838f20"], "status": "REASONED"}, + "supabase-storage-policies": {"text": "Authorize storage.objects separately by bucket and owner path; upload needs INSERT, upsert also SELECT/UPDATE, and deletion needs deliberate permission.", "components": ["supabase"], "sources": ["supabase:sdba7d7b6dcf8", "supabase:s9e1484f46eff", "supabase:see2d3be99ebd"], "status": "REASONED"}, + "supabase-realtime-default": {"text": "Public Realtime channels are allowed by default; disable them project-wide and also set client config.private.", "components": ["supabase"], "sources": ["supabase:s5674b1057e14"], "status": "REASONED"}, + "supabase-realtime-policy": {"text": "Broadcast/Presence receive/send need separate SELECT/INSERT policies on realtime.messages, constrained by topic ownership and extension.", "components": ["supabase"], "sources": ["supabase:s84efad0ba39d"], "status": "REASONED"}, + "supabase-changes": {"text": "Postgres Changes uses source-table authorization; private-channel policies are not universal and DELETE has separate limitations.", "components": ["supabase"], "sources": ["supabase:s694eab64f2ec"], "status": "REASONED"}, + "supabase-signup": {"text": "Disable signup, anonymous sign-ins and unused providers when unwanted; inspect hosted settings instead of assuming universal defaults.", "components": ["supabase"], "sources": ["supabase:sc797f0b63472"], "status": "REASONED"}, + "supabase-cli-auth": {"text": "CLI enable_signup defaults true and enable_anonymous_sign_ins false; local config edits do not prove hosted settings changed.", "components": ["supabase"], "sources": ["supabase:s3a5e39fe7b4d"], "status": "REASONED"}, + "supabase-mfa": {"text": "TOTP is available on every plan at writing; enrollment alone does not constrain data. Require aal2 using a restrictive policy alongside permissive ownership policies.", "components": ["supabase", "postgres"], "sources": ["supabase:s182755725d74", "supabase:s741468ff35a9", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, + "supabase-edge-jwt": {"text": "Per-function verify_jwt defaults true; --no-verify-jwt disables platform checking and requires handler authentication before protected work.", "components": ["supabase"], "sources": ["supabase:s7dfbb47b5d20", "supabase:sf7d438b8c080", "supabase:se52bbd097033"], "status": "REASONED"}, + "supabase-edge-auth": {"text": "Platform JWT success is not resource authorization and API-key compatibility can pass it; separately authorize users or verify webhook signatures.", "components": ["supabase"], "sources": ["supabase:sf7d438b8c080", "supabase:se52bbd097033"], "status": "REASONED"}, + "supabase-edge-secrets": {"text": "Edge environments contain privileged credentials including SUPABASE_SERVICE_ROLE_KEY; protect managed/local secrets, responses, logs and client bundles.", "components": ["supabase"], "sources": ["supabase:sb3c4c0993e14"], "status": "REASONED"}, + "verify-firestore": {"text": "Compare unsigned/A/B reads and writes to user documents and descendants, with a successful owner control; test parent documents to distinguish recursive-rule versions.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, + "verify-rtdb": {"text": "Compare unsigned/A/B reads/writes to seeded RTDB owner paths; ancestor grants expose them while ownership rules deny unauthorized callers.", "components": ["firebase"], "sources": ["firebase:sa6b1c4933e6a"], "status": "REASONED"}, + "verify-firebase-storage": {"text": "Compare unsigned/A/B object reads and uploads; ownership rules must deny unrelated callers while owner operations succeed.", "components": ["firebase"], "sources": ["firebase:s0a7f54463abd"], "status": "REASONED"}, + "verify-download-token": {"text": "Same sessionless download URL works after Rules tightening but must lose bytes after token revocation; owner getBytes remains successful and network/CORS failures are inconclusive.", "components": ["firebase"], "sources": ["firebase:s9b7e57b472c7", "firebase:sa0584b797732", "firebase:sfd1e7412abf6"], "status": "REASONED"}, + "verify-tables": {"text": "Pair unsigned/A/B profiles GETs against seeded rows, using ordinary credentials and required MFA; test intended writes and persisted effects separately.", "components": ["supabase", "postgres", "curl"], "sources": ["supabase:s92b405dc3c7e", "postgres:se9846041ce1f", "supabase:s741468ff35a9", "curl:s5e1383feef7d", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, + "verify-views": {"text": "Compare view reads with table boundaries; owner-privileged exposure must close under invoker/revoked access while intended reads or deliberate removal are confirmed.", "components": ["postgres"], "sources": ["postgres:s13c54ab9cc6b"], "status": "REASONED"}, + "verify-rpc": {"text": "Test every API function overload with valid harmless arguments as unsigned/A/B; unauthorized data or operations must be denied while the intended caller succeeds.", "components": ["supabase"], "sources": ["supabase:s087c8cf42ab0", "supabase:s8405a9fb4b74"], "status": "REASONED"}, + "verify-http": {"text": "Probe HTTP wrappers with a controlled collector and marker; unauthorized fixed calls must emit no request. Commit pg_net fixture transactions before expecting delivery.", "components": ["supabase"], "sources": ["supabase:s76740bffcb32"], "status": "REASONED"}, + "verify-vault": {"text": "Use a disposable Vault canary to compare exposed/fixed wrappers; inspect function and table privileges as supporting evidence.", "components": ["supabase", "postgres"], "sources": ["supabase:s47bb8fa3e14e", "postgres:sc39eceeac711"], "status": "REASONED"}, + "verify-storage": {"text": "Compare public URL and A/B private downloads plus intended uploads/upserts; unauthorized callers must receive no bytes or cross-owner writes.", "components": ["supabase"], "sources": ["supabase:sc29febbb230e", "supabase:see2d3be99ebd", "supabase:s1567fb838f20"], "status": "REASONED"}, + "verify-realtime": {"text": "Compare A/B join, Broadcast and Presence delivery; public-disabled clients lacking private=true must fail, and connection alone is insufficient.", "components": ["supabase"], "sources": ["supabase:s5674b1057e14", "supabase:s84efad0ba39d"], "status": "REASONED"}, + "verify-changes": {"text": "Observe A/B INSERT/UPDATE subscription events against seeded rows; fixed table authorization withholds unrelated events. Evaluate DELETE separately.", "components": ["supabase"], "sources": ["supabase:s694eab64f2ec"], "status": "REASONED"}, + "verify-edge": {"text": "Compare no/invalid Bearer without apikey, authorized owners and valid webhook signatures; distinguish platform 401 from handler authorization and inspect effects/secrets.", "components": ["supabase"], "sources": ["supabase:se52bbd097033", "supabase:sf7d438b8c080", "supabase:sb3c4c0993e14"], "status": "REASONED"}, + "verify-firebase-functions": {"text": "Compare callable requests with valid App Check but missing user auth and A/B operations; HTTP endpoints need separate invalid/valid user or IAM comparisons.", "components": ["firebase"], "sources": ["firebase:s79da8f65f90a", "firebase:s49f78d054708"], "status": "REASONED"}, + "verify-appcheck": {"text": "Repeat owner requests with/without App Check per product/callable; enforcement must deny missing attestation while compatible clients work.", "components": ["firebase"], "sources": ["firebase:sba7129c83405", "firebase:sb0ff8f1726f7"], "status": "REASONED"}, + "verify-api-restrictions": {"text": "Test intended versus excluded apps/referrers after key restrictions, retaining successful intended product traffic.", "components": ["firebase"], "sources": ["firebase:safeed4cd2b67", "firebase:sc78d5eb59b65"], "status": "REASONED"}, + "verify-emulators": {"text": "Enumerate hub /emulators and sockets, compare missing versus loaded Rules and second-host connectivity, and retain local owner controls.", "components": ["firebase"], "sources": ["firebase:sd8a1bda29f5c", "firebase:s69d9721c7f29"], "status": "REASONED"}, + "verify-firebase-accounts": {"text": "Sign out before each Anonymous signup test and check isNewUser; disabled providers must reject creation. Enumeration tests compare sign-in errors, not EMAIL_EXISTS signup behavior.", "components": ["firebase"], "sources": ["firebase:s8c90f9f6c7fa", "firebase:s8c8336ec252e", "firebase:s142e56472093", "firebase:s77e2cbd8a29a", "firebase:s06808e4b6965"], "status": "REASONED"}, + "verify-supabase-accounts": {"text": "Fresh signup and anonymous creation must fail under invite-only/anonymous-disabled settings while an invited account still signs in.", "components": ["supabase"], "sources": ["supabase:sc797f0b63472", "supabase:sa2dd4637969e", "supabase:sad79d4d1b6ee"], "status": "REASONED"}, + "verify-mfa": {"text": "Owner profiles GET with aal1 must yield no protected rows under the restrictive policy, while fresh aal2 returns the seeded owner row.", "components": ["supabase"], "sources": ["supabase:s741468ff35a9"], "status": "REASONED", "verify": [1]}, + "verify-key-retirement": {"text": "Compare old/new API keys after explicit deactivation and old/new JWTs after revocation, accounting for propagation and caches.", "components": ["supabase"], "sources": ["supabase:sa946354db383", "supabase:sfc2f9ae0232b"], "status": "REASONED"}, + "verify-api-removal": {"text": "Previously successful profiles requests must stop returning fixtures after schema removal or Data API disabling; retained schemas need authorized controls.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3", "supabase:s20ef046ac646"], "status": "REASONED"}, + "verify-bundle": {"text": "Filename-only service_role/eyJ/sb_secret_ scanning yields review candidates, not proof of leaks; public anon JWTs are legitimate. Use a marker control and inspect encoded/split secrets separately.", "components": ["supabase"], "sources": ["supabase:s73d939b4644e"], "status": "REASONED", "verify": [2]}, + "local-guards": {"text": "Historical local guard tests rejected placeholders and malformed set lines; this guide's assessment is shell behavior only, not live authorization. The recorded ShellCheck 0.11.0 has a release citation, not host-binary evidence; bash and other tool/parser versions remain unrecorded.", "components": ["supabase", "shellcheck"], "sources": ["supabase:s92b405dc3c7e", "supabase:s73d939b4644e", "shellcheck:sf994764bf270"], "status": "DEMONSTRATED", "evidence": "Local guard tests refused embedded `REPLACE_WITH_` placeholders, `example.com`, angle brackets, empty values, and omitted or shortened `set --` lines."}, + "local-syntax": {"text": "Recorded ShellCheck 0.11.0/bash and JSON/TOML/JavaScript parsing passed; this guide's assessment is that these checks do not compile Firebase Rules, execute SQL or demonstrate services. Only the ShellCheck release is cited; bash and other tool/parser versions remain unrecorded.", "components": ["firebase", "supabase", "shellcheck"], "sources": ["firebase:sa6b1c4933e6a", "firebase:sd8a1bda29f5c", "supabase:s3a5e39fe7b4d", "firebase:sc8f214d059d2", "shellcheck:sf994764bf270"], "status": "DEMONSTRATED", "evidence": "The RTDB JSON, `firebase.json` fragment, Auth TOML, and callable JavaScript passed local parsing checks."} + } +} +```` + diff --git a/free-certificates.md b/free-certificates.md index 1c4f8c880..3299054d3 100644 --- a/free-certificates.md +++ b/free-certificates.md @@ -1,225 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-17", - "documentation_checked": "2026-09", - "body_sha256": "1f578874874b2d4b757f649adda8745f4d241949ddc0c71bfffb7315a7158257", - "components": { - "le": { - "name": "Let's Encrypt documentation", - "basis": "unknown", - "sources": { - "s9ff2d787c618": "https://letsencrypt.org/docs/", - "s01388c85c5d6": "https://letsencrypt.org/docs/cert-lifetimes/", - "s4406ef210fff": "https://letsencrypt.org/2025/12/02/from-90-to-45", - "s21efa5995c48": "https://letsencrypt.org/2025/08/06/ocsp-service-has-reached-end-of-life", - "saf908bcd2143": "https://letsencrypt.org/docs/caa/" - } - }, - "certbot": { - "name": "Certbot documentation", - "basis": "unknown", - "sources": { - "s44faaf4eb7d7": "https://certbot.eff.org/", - "s7b60a00cadf3": "https://eff-certbot.readthedocs.io/en/stable/using.html#manual", - "s875be163b4c1": "https://eff-certbot.readthedocs.io/en/stable/using.html#dns-plugins" - } - }, - "reconfigure": { - "name": "Certbot reconfigure minimum", - "basis": "2.3.0", - "sources": { - "s0dee7d6d65ba": "https://raw.githubusercontent.com/certbot/certbot/v2.3.0/certbot/docs/using.rst" - } - }, - "zerossl": { - "name": "ZeroSSL", - "basis": "unknown", - "sources": { - "sddca28a3381b": "https://zerossl.com/" - } - }, - "acme": { - "name": "acme.sh", - "basis": "unknown", - "sources": { - "sa635c50fc1f9": "https://github.com/acmesh-official/acme.sh" - } - }, - "caddy": { - "name": "Caddy automatic HTTPS", - "basis": "v2.11.4", - "sources": { - "s9683cb8d689f": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L62-L63", - "s409f9221aec2": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L280-L296", - "s80dbf0751153": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L245-L249", - "s5216789c9341": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L321-L328" - } - }, - "certmagic": { - "name": "certmagic ACME implementation", - "basis": "v0.25.3", - "sources": { - "s61e3cba719df": "https://github.com/caddyserver/caddy/blob/v2.11.4/go.mod#L12-L12", - "s8782e006523b": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeclient.go#L188-L226", - "s465c4ddcc92f": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeclient.go#L304-L313", - "sc1e2ddefe5ed": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L116-L120", - "sd423f53faa1a": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L222-L236", - "sf2fbc79f6542": "https://github.com/caddyserver/certmagic/blob/v0.25.3/certmagic.go#L473-L475", - "s1c3236df58b9": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L647-L654" - } - }, - "traefik": { - "name": "Traefik ACME documentation", - "basis": "v3.5", - "sources": { - "s418a6b1ad09b": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/tls/certificate-resolvers/acme/" - } - }, - "openssl-cli": { - "name": "OpenSSL certificate commands", - "basis": "3.0", - "sources": { - "s00aaf106164f": "https://docs.openssl.org/3.0/man1/openssl-s_client/", - "s0856f4a58574": "https://docs.openssl.org/3.0/man1/openssl-x509/" - } - }, - "curl-source": { - "name": "curl command documentation", - "basis": "curl-8_12_1", - "sources": { - "s76f69702c9d5": "https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/head.md#L21-L22", - "s60820cd231e1": "https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/insecure.md#L21-L28" - } - }, - "certbot-v230": { - "name": "Certbot user guide and source", - "basis": "v2.3.0", - "sources": { - "s725109195132": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L808-L825", - "s2f25bc233392": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L632-L640", - "s11c7994242fc": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L704-L710", - "sbb0c60d635ee": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L684", - "s55ec19f37395": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L194-L220", - "s5bf4ff9fbe76": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L222-L265", - "s7a053488f705": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/cli_utils.py#L227-L231", - "sa18c858c4e49": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/renewal.py#L40-L53", - "s2e4bd79a7795": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/renewal.py#L509-L534", - "s1910987bfd8c": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L295-L312", - "s9c6118b72e0a": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L1120-L1128", - "s3e22e32a2862": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L1243-L1249" - } - }, - "certbot-dns-cloudflare-v230": { - "name": "Certbot Cloudflare DNS plugin", - "basis": "v2.3.0", - "sources": { - "s3f07ef3d544d": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py#L24-L39", - "sc3b33fa2c90f": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py#L61-L78" - } - }, - "cloudflare-origin-ca": { - "name": "Cloudflare Origin CA documentation", - "basis": "checked October 2026", - "sources": { - "s1e15de5c9f36": "https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/" - } - } - }, - "claims": { - "public-trust": {"text": "ACME CAs such as Let's Encrypt and ZeroSSL issue free publicly trusted certificates; this guide advises preferring them for public DNS names, including private hosts using DNS-01.", "components": ["le", "zerossl"], "sources": ["le:s9ff2d787c618", "zerossl:sddca28a3381b"], "status": "REASONED"}, - "http-challenge": {"text": "HTTP-01 needs a public A/AAAA or CNAME pointing to the server and inbound internet access to port 80.", "components": ["le"], "sources": ["le:s9ff2d787c618"], "status": "REASONED"}, - "alpn-challenge": {"text": "TLS-ALPN-01 needs a public DNS record and inbound port 443; Caddy's ACME issuer enables the TLS-ALPN-01 solver when DNS-01 is not configured and TLS-ALPN-01 is not disabled, and can use an alternate local port if port 443 forwards to it; Traefik also uses TLS-ALPN-01.", "components": ["le", "caddy", "certmagic", "traefik"], "sources": ["le:s9ff2d787c618", "caddy:s409f9221aec2", "caddy:s80dbf0751153", "caddy:s5216789c9341", "certmagic:s61e3cba719df", "certmagic:s8782e006523b", "certmagic:s465c4ddcc92f", "certmagic:sc1e2ddefe5ed", "certmagic:sd423f53faa1a", "certmagic:sf2fbc79f6542", "certmagic:s1c3236df58b9", "traefik:s418a6b1ad09b"], "status": "REASONED"}, - "dns-challenge": {"text": "DNS-01 needs control of the challenge TXT record, no public address record or inbound port, and supports wildcards; automated issuance and renewal need an automated way to update TXT records, and this guide advises a provider DNS plugin and scoped API credentials, while Certbot also supports manual DNS-01 and an authentication hook for automatic renewal.", "components": ["le", "certbot", "certbot-v230"], "sources": ["le:s9ff2d787c618", "certbot:s7b60a00cadf3", "certbot:s875be163b4c1", "certbot-v230:s55ec19f37395", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"}, - "certbot-install": {"text": "Install Certbot and nginx/Apache plugins from the distribution, or the classic snap with /usr/bin/certbot pointing to /snap/bin/certbot.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, - "server-plugins": {"text": "certbot --nginx or --apache issues and installs certificates for the supplied domain names when the server is supported.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, - "standalone": {"text": "certonly --standalone binds port 80; stop its current owner and arrange webroot, a server plugin or port-freeing hooks for unattended renewal.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, - "webroot": {"text": "certonly --webroot uses /var/www/html to serve challenge files while the existing web server keeps running.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, - "dns-credentials": {"text": "The Cloudflare DNS plugin uses a credentials file and a Zone:DNS:Edit token limited to required zones; this guide requires never using the Global API Key and protecting the file with mode 600 in a 700 directory.", "components": ["certbot", "certbot-dns-cloudflare-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-dns-cloudflare-v230:s3f07ef3d544d", "certbot-dns-cloudflare-v230:sc3b33fa2c90f"], "status": "REASONED"}, - "certificate-paths": {"text": "Configure servers against stable /etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem paths for the chain and private key.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, - "renewal-schedule": {"text": "The Certbot v2.3.0 user guide says most installations have a scheduled task running certbot renew and says that, on Linux and BSD, you can check cron or systemd timers if unsure; this guide requires confirming the task and testing renewal plus reload with --dry-run --run-deploy-hooks, including package and snap installations.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-v230:s725109195132"], "status": "REASONED"}, - "deploy-hook": {"text": "This guide requires persisting a reload hook during successful initial issuance or successful renewal, or installing an executable in renewal-hooks/deploy; Certbot v2.3.0 saves the renewal-relevant options set on the command line to a lineage after successful renewal, and a renew --deploy-hook invocation with no renewal due does not save that hook.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-v230:s2f25bc233392", "certbot-v230:s11c7994242fc", "certbot-v230:sbb0c60d635ee", "certbot-v230:s7a053488f705", "certbot-v230:sa18c858c4e49", "certbot-v230:s2e4bd79a7795", "certbot-v230:s1910987bfd8c", "certbot-v230:s9c6118b72e0a", "certbot-v230:s3e22e32a2862"], "status": "REASONED"}, - "reconfigure": {"text": "certbot reconfigure can add the reload hook later on Certbot 2.3.0 and later.", "components": ["reconfigure"], "sources": ["reconfigure:s0dee7d6d65ba"], "status": "REASONED"}, - "expiry-monitoring": {"text": "Monitor the served certificate's expiry and renewal failures instead of relying on CA reminder emails; include a dry run in the deployment checklist. This guide assesses that any renewal step involving a person will fail at those scheduled shorter certificate lifetimes and that unattended automation is the only viable path.", "components": ["le", "certbot"], "sources": ["le:s4406ef210fff", "certbot:s44faaf4eb7d7"], "status": "REASONED"}, - "default-lifetime": {"text": "At the guide's September 2026 documentation check, Let's Encrypt's default certificate lifetime is 90 days.", "components": ["le"], "sources": ["le:s01388c85c5d6"], "status": "REASONED"}, - "short-lifetime": {"text": "Optional 6-day short-lived certificates are available to every subscriber as of the guide's September 2026 check.", "components": ["le"], "sources": ["le:s01388c85c5d6"], "status": "REASONED"}, - "lifetime-schedule": {"text": "The classic profile moves to 64 days on 2027-02-10 and 45 days on 2028-02-16; industry rules cap publicly trusted certificates at 47 days from 2029-03-15.", "components": ["le"], "sources": ["le:s01388c85c5d6", "le:s4406ef210fff"], "status": "REASONED"}, - "revocation": {"text": "Let's Encrypt ended OCSP on 2025-08-06 and publishes revocation through CRLs only; do not add OCSP stapling directives for its certificates.", "components": ["le"], "sources": ["le:s21efa5995c48"], "status": "REASONED"}, - "staging": {"text": "Let's Encrypt limits per-domain issuance; test with certbot --staging or --test-cert before real issuance.", "components": ["le", "certbot"], "sources": ["le:s9ff2d787c618", "certbot:s44faaf4eb7d7"], "status": "REASONED"}, - "caa": {"text": "CAA restricts issuing CAs, using issue letsencrypt.org for Let's Encrypt; certificate-transparency monitoring detects mis-issuance afterwards and complements CAA.", "components": ["le"], "sources": ["le:saf908bcd2143", "le:s9ff2d787c618"], "status": "REASONED"}, - "zerossl-eab": {"text": "ZeroSSL offers free ACME certificates; some clients need dashboard EAB credentials.", "components": ["zerossl"], "sources": ["zerossl:sddca28a3381b"], "status": "REASONED"}, - "acme-default": {"text": "acme.sh registers with ZeroSSL by default.", "components": ["acme"], "sources": ["acme:sa635c50fc1f9"], "status": "REASONED"}, - "acme-install": {"text": "acme.sh is a shell ACME client supporting many DNS providers; the guide recommends installation from a repository release instead of piping a download into a shell.", "components": ["acme"], "sources": ["acme:sa635c50fc1f9"], "status": "REASONED"}, - "server-automation": {"text": "This guide advises Caddy or Traefik for built-in issuance and renewal without an external client; it assesses them as the lowest-effort correct option for new deployments.", "components": ["le", "caddy", "traefik"], "sources": ["le:s9ff2d787c618", "caddy:s9683cb8d689f", "traefik:s418a6b1ad09b"], "status": "REASONED"}, - "origin-certificates": {"text": "Cloudflare lists Origin CA certificates on its Free plan, refers to long-lived origin CA certificates and says the validity period is chosen at creation, says they only encrypt traffic between Cloudflare and the origin, and warns that visitors may see untrusted certificate errors when Cloudflare is paused or proxying is disabled; this guide requires using them only for origins that receive traffic only through Cloudflare-proxied records.", "components": ["le", "cloudflare-origin-ca"], "sources": ["le:s9ff2d787c618", "cloudflare-origin-ca:s1e15de5c9f36"], "status": "REASONED"}, - "verify-inventory": {"text": "certbot certificates lists issued certificates and their expiry dates.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED", "verify": [1]}, - "verify-https": {"text": "Use the deployment's public hostname rather than example.com; curl HTTPS should succeed without -k.", "components": ["le", "curl-source"], "sources": ["le:s9ff2d787c618", "curl-source:s76f69702c9d5", "curl-source:s60820cd231e1"], "status": "REASONED", "verify": [1]}, - "verify-certificate": {"text": "The OpenSSL probe targets the hostname on port 443 with SNI, hostname verification and verification errors enabled, then displays issuer and dates.", "components": ["le", "openssl-cli"], "sources": ["le:s9ff2d787c618", "openssl-cli:s00aaf106164f", "openssl-cli:s0856f4a58574"], "status": "REASONED", "verify": [1]}, - "authentication": {"text": "A certificate alone is insufficient protection; continue with server controls and authentication.", "components": ["le"], "sources": ["le:s9ff2d787c618"], "status": "REASONED"}, - "dns-automation": {"text": "For automated DNS-01 issuance and renewal, this guide advises using a provider DNS plugin and least-privilege API credentials; Certbot also documents manual TXT entry and authentication hooks that automate the challenge.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s875be163b4c1", "certbot:s7b60a00cadf3", "certbot-v230:s55ec19f37395", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"}, - "dns-manual": {"text": "certbot certonly --manual --preferred-challenges dns permits hand-created TXT records; the Certbot v2.3.0 guide says certificates created with --manual do not support automatic renewal without a --manual-auth-hook that automates the challenge.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s7b60a00cadf3", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"} - } -} ---- # Free publicly trusted certificates (ACME) - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +Written against Let's Encrypt documentation (no recorded version), Certbot reconfigure minimum 2.3.0, Certbot user guide and source v2.3.0, Certbot Cloudflare DNS plugin v2.3.0, Caddy automatic HTTPS v2.11.4, certmagic ACME implementation v0.25.3, and Traefik ACME documentation v3.5, plus 3 more recorded bases; 3 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 31 claims, all reasoned from cited sources. -Metadata reviewed 2026-10-17; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- le: Let's Encrypt documentation (unknown) -- certbot: Certbot documentation (unknown) -- reconfigure: Certbot reconfigure minimum (2.3.0) -- zerossl: ZeroSSL (unknown) -- acme: acme.sh (unknown) -- caddy: Caddy automatic HTTPS (v2.11.4) -- certmagic: certmagic ACME implementation (v0.25.3) -- traefik: Traefik ACME documentation (v3.5) -- openssl-cli: OpenSSL certificate commands (3.0) -- curl-source: curl command documentation (curl-8_12_1) -- certbot-v230: Certbot user guide and source (v2.3.0) -- certbot-dns-cloudflare-v230: Certbot Cloudflare DNS plugin (v2.3.0) -- cloudflare-origin-ca: Cloudflare Origin CA documentation (checked October 2026) - -Claims (full source mapping in the front matter): - -- public-trust (REASONED; le, zerossl): ACME CAs such as Let's Encrypt and ZeroSSL issue free publicly trusted certificates; this guide advises preferring them for public DNS names, including private hosts using DNS-01. -- http-challenge (REASONED; le): HTTP-01 needs a public A/AAAA or CNAME pointing to the server and inbound internet access to port 80. -- alpn-challenge (REASONED; le, caddy, certmagic, traefik): TLS-ALPN-01 needs a public DNS record and inbound port 443; Caddy's ACME issuer enables the TLS-ALPN-01 solver when DNS-01 is not configured and TLS-ALPN-01 is not disabled, and can use an alternate local port if port 443 forwards to it; Traefik also uses TLS-ALPN-01. -- dns-challenge (REASONED; le, certbot, certbot-v230): DNS-01 needs control of the challenge TXT record, no public address record or inbound port, and supports wildcards; automated issuance and renewal need an automated way to update TXT records, and this guide advises a provider DNS plugin and scoped API credentials, while Certbot also supports manual DNS-01 and an authentication hook for automatic renewal. -- certbot-install (REASONED; certbot): Install Certbot and nginx/Apache plugins from the distribution, or the classic snap with /usr/bin/certbot pointing to /snap/bin/certbot. -- server-plugins (REASONED; certbot): certbot --nginx or --apache issues and installs certificates for the supplied domain names when the server is supported. -- standalone (REASONED; certbot): certonly --standalone binds port 80; stop its current owner and arrange webroot, a server plugin or port-freeing hooks for unattended renewal. -- webroot (REASONED; certbot): certonly --webroot uses /var/www/html to serve challenge files while the existing web server keeps running. -- dns-credentials (REASONED; certbot, certbot-dns-cloudflare-v230): The Cloudflare DNS plugin uses a credentials file and a Zone:DNS:Edit token limited to required zones; this guide requires never using the Global API Key and protecting the file with mode 600 in a 700 directory. -- certificate-paths (REASONED; certbot): Configure servers against stable /etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem paths for the chain and private key. -- renewal-schedule (REASONED; certbot, certbot-v230): The Certbot v2.3.0 user guide says most installations have a scheduled task running certbot renew and says that, on Linux and BSD, you can check cron or systemd timers if unsure; this guide requires confirming the task and testing renewal plus reload with --dry-run --run-deploy-hooks, including package and snap installations. -- deploy-hook (REASONED; certbot, certbot-v230): This guide requires persisting a reload hook during successful initial issuance or successful renewal, or installing an executable in renewal-hooks/deploy; Certbot v2.3.0 saves the renewal-relevant options set on the command line to a lineage after successful renewal, and a renew --deploy-hook invocation with no renewal due does not save that hook. -- reconfigure (REASONED; reconfigure): certbot reconfigure can add the reload hook later on Certbot 2.3.0 and later. -- expiry-monitoring (REASONED; le, certbot): Monitor the served certificate's expiry and renewal failures instead of relying on CA reminder emails; include a dry run in the deployment checklist. This guide assesses that any renewal step involving a person will fail at those scheduled shorter certificate lifetimes and that unattended automation is the only viable path. -- default-lifetime (REASONED; le): At the guide's September 2026 documentation check, Let's Encrypt's default certificate lifetime is 90 days. -- short-lifetime (REASONED; le): Optional 6-day short-lived certificates are available to every subscriber as of the guide's September 2026 check. -- lifetime-schedule (REASONED; le): The classic profile moves to 64 days on 2027-02-10 and 45 days on 2028-02-16; industry rules cap publicly trusted certificates at 47 days from 2029-03-15. -- revocation (REASONED; le): Let's Encrypt ended OCSP on 2025-08-06 and publishes revocation through CRLs only; do not add OCSP stapling directives for its certificates. -- staging (REASONED; le, certbot): Let's Encrypt limits per-domain issuance; test with certbot --staging or --test-cert before real issuance. -- caa (REASONED; le): CAA restricts issuing CAs, using issue letsencrypt.org for Let's Encrypt; certificate-transparency monitoring detects mis-issuance afterwards and complements CAA. -- zerossl-eab (REASONED; zerossl): ZeroSSL offers free ACME certificates; some clients need dashboard EAB credentials. -- acme-default (REASONED; acme): acme.sh registers with ZeroSSL by default. -- acme-install (REASONED; acme): acme.sh is a shell ACME client supporting many DNS providers; the guide recommends installation from a repository release instead of piping a download into a shell. -- server-automation (REASONED; le, caddy, traefik): This guide advises Caddy or Traefik for built-in issuance and renewal without an external client; it assesses them as the lowest-effort correct option for new deployments. -- origin-certificates (REASONED; le, cloudflare-origin-ca): Cloudflare lists Origin CA certificates on its Free plan, refers to long-lived origin CA certificates and says the validity period is chosen at creation, says they only encrypt traffic between Cloudflare and the origin, and warns that visitors may see untrusted certificate errors when Cloudflare is paused or proxying is disabled; this guide requires using them only for origins that receive traffic only through Cloudflare-proxied records. -- verify-inventory (REASONED; certbot): certbot certificates lists issued certificates and their expiry dates. -- verify-https (REASONED; le, curl-source): Use the deployment's public hostname rather than example.com; curl HTTPS should succeed without -k. -- verify-certificate (REASONED; le, openssl-cli): The OpenSSL probe targets the hostname on port 443 with SNI, hostname verification and verification errors enabled, then displays issuer and dates. -- authentication (REASONED; le): A certificate alone is insufficient protection; continue with server controls and authentication. -- dns-automation (REASONED; certbot, certbot-v230): For automated DNS-01 issuance and renewal, this guide advises using a provider DNS plugin and least-privilege API credentials; Certbot also documents manual TXT entry and authentication hooks that automate the challenge. -- dns-manual (REASONED; certbot, certbot-v230): certbot certonly --manual --preferred-challenges dns permits hand-created TXT records; the Certbot v2.3.0 guide says certificates created with --manual do not support automatic renewal without a --manual-auth-hook that automates the challenge. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Publicly trusted certificates are free through ACME certificate authorities such as Let's Encrypt and ZeroSSL. Browsers and libraries accept them without any client-side configuration, which is why this guide assesses them as the correct choice for every service with a public DNS name. Use [self-signed.md](self-signed.md) only when no public domain exists; a host that cannot accept inbound connections can still get a publicly trusted certificate through the DNS-01 challenge (below), or serve behind [cloudflare.md](cloudflare.md). @@ -364,3 +147,246 @@ REASONED: preceding block; certificate inventory, HTTPS access and served-certif - Certbot v2.3.0 Cloudflare DNS credentials (checked October 2026): - Certbot v2.3.0 Cloudflare DNS credential permissions (checked October 2026): - Cloudflare Origin CA (checked October 2026): https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/ + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/free-certificates.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- le: Let's Encrypt documentation (unknown) +- zerossl: ZeroSSL (unknown) +- certbot: Certbot documentation (unknown) +- reconfigure: Certbot reconfigure minimum (2.3.0) +- acme: acme.sh (unknown) +- certbot-v230: Certbot user guide and source (v2.3.0) +- certbot-dns-cloudflare-v230: Certbot Cloudflare DNS plugin (v2.3.0) +- caddy: Caddy automatic HTTPS (v2.11.4) +- certmagic: certmagic ACME implementation (v0.25.3) +- traefik: Traefik ACME documentation (v3.5) +- cloudflare-origin-ca: Cloudflare Origin CA documentation (checked October 2026) +- openssl-cli: OpenSSL certificate commands (3.0) +- curl-source: curl command documentation (curl-8_12_1) + +Claims (full source mapping in the JSON block below): + +- public-trust (REASONED; le, zerossl): ACME CAs such as Let's Encrypt and ZeroSSL issue free publicly trusted certificates; this guide advises preferring them for public DNS names, including private hosts using DNS-01. +- http-challenge (REASONED; le): HTTP-01 needs a public A/AAAA or CNAME pointing to the server and inbound internet access to port 80. +- alpn-challenge (REASONED; le, caddy, certmagic, traefik): TLS-ALPN-01 needs a public DNS record and inbound port 443; Caddy's ACME issuer enables the TLS-ALPN-01 solver when DNS-01 is not configured and TLS-ALPN-01 is not disabled, and can use an alternate local port if port 443 forwards to it; Traefik also uses TLS-ALPN-01. +- dns-challenge (REASONED; le, certbot, certbot-v230): DNS-01 needs control of the challenge TXT record, no public address record or inbound port, and supports wildcards; automated issuance and renewal need an automated way to update TXT records, and this guide advises a provider DNS plugin and scoped API credentials, while Certbot also supports manual DNS-01 and an authentication hook for automatic renewal. +- certbot-install (REASONED; certbot): Install Certbot and nginx/Apache plugins from the distribution, or the classic snap with /usr/bin/certbot pointing to /snap/bin/certbot. +- server-plugins (REASONED; certbot): certbot --nginx or --apache issues and installs certificates for the supplied domain names when the server is supported. +- standalone (REASONED; certbot): certonly --standalone binds port 80; stop its current owner and arrange webroot, a server plugin or port-freeing hooks for unattended renewal. +- webroot (REASONED; certbot): certonly --webroot uses /var/www/html to serve challenge files while the existing web server keeps running. +- dns-credentials (REASONED; certbot, certbot-dns-cloudflare-v230): The Cloudflare DNS plugin uses a credentials file and a Zone:DNS:Edit token limited to required zones; this guide requires never using the Global API Key and protecting the file with mode 600 in a 700 directory. +- certificate-paths (REASONED; certbot): Configure servers against stable /etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem paths for the chain and private key. +- renewal-schedule (REASONED; certbot, certbot-v230): The Certbot v2.3.0 user guide says most installations have a scheduled task running certbot renew and says that, on Linux and BSD, you can check cron or systemd timers if unsure; this guide requires confirming the task and testing renewal plus reload with --dry-run --run-deploy-hooks, including package and snap installations. +- deploy-hook (REASONED; certbot, certbot-v230): This guide requires persisting a reload hook during successful initial issuance or successful renewal, or installing an executable in renewal-hooks/deploy; Certbot v2.3.0 saves the renewal-relevant options set on the command line to a lineage after successful renewal, and a renew --deploy-hook invocation with no renewal due does not save that hook. +- reconfigure (REASONED; reconfigure): certbot reconfigure can add the reload hook later on Certbot 2.3.0 and later. +- expiry-monitoring (REASONED; le, certbot): Monitor the served certificate's expiry and renewal failures instead of relying on CA reminder emails; include a dry run in the deployment checklist. This guide assesses that any renewal step involving a person will fail at those scheduled shorter certificate lifetimes and that unattended automation is the only viable path. +- default-lifetime (REASONED; le): At the guide's September 2026 documentation check, Let's Encrypt's default certificate lifetime is 90 days. +- short-lifetime (REASONED; le): Optional 6-day short-lived certificates are available to every subscriber as of the guide's September 2026 check. +- lifetime-schedule (REASONED; le): The classic profile moves to 64 days on 2027-02-10 and 45 days on 2028-02-16; industry rules cap publicly trusted certificates at 47 days from 2029-03-15. +- revocation (REASONED; le): Let's Encrypt ended OCSP on 2025-08-06 and publishes revocation through CRLs only; do not add OCSP stapling directives for its certificates. +- staging (REASONED; le, certbot): Let's Encrypt limits per-domain issuance; test with certbot --staging or --test-cert before real issuance. +- caa (REASONED; le): CAA restricts issuing CAs, using issue letsencrypt.org for Let's Encrypt; certificate-transparency monitoring detects mis-issuance afterwards and complements CAA. +- zerossl-eab (REASONED; zerossl): ZeroSSL offers free ACME certificates; some clients need dashboard EAB credentials. +- acme-default (REASONED; acme): acme.sh registers with ZeroSSL by default. +- acme-install (REASONED; acme): acme.sh is a shell ACME client supporting many DNS providers; the guide recommends installation from a repository release instead of piping a download into a shell. +- server-automation (REASONED; le, caddy, traefik): This guide advises Caddy or Traefik for built-in issuance and renewal without an external client; it assesses them as the lowest-effort correct option for new deployments. +- origin-certificates (REASONED; le, cloudflare-origin-ca): Cloudflare lists Origin CA certificates on its Free plan, refers to long-lived origin CA certificates and says the validity period is chosen at creation, says they only encrypt traffic between Cloudflare and the origin, and warns that visitors may see untrusted certificate errors when Cloudflare is paused or proxying is disabled; this guide requires using them only for origins that receive traffic only through Cloudflare-proxied records. +- verify-inventory (REASONED; certbot): certbot certificates lists issued certificates and their expiry dates. +- verify-https (REASONED; le, curl-source): Use the deployment's public hostname rather than example.com; curl HTTPS should succeed without -k. +- verify-certificate (REASONED; le, openssl-cli): The OpenSSL probe targets the hostname on port 443 with SNI, hostname verification and verification errors enabled, then displays issuer and dates. +- authentication (REASONED; le): A certificate alone is insufficient protection; continue with server controls and authentication. +- dns-automation (REASONED; certbot, certbot-v230): For automated DNS-01 issuance and renewal, this guide advises using a provider DNS plugin and least-privilege API credentials; Certbot also documents manual TXT entry and authentication hooks that automate the challenge. +- dns-manual (REASONED; certbot, certbot-v230): certbot certonly --manual --preferred-challenges dns permits hand-created TXT records; the Certbot v2.3.0 guide says certificates created with --manual do not support automatic renewal without a --manual-auth-hook that automates the challenge. + +````json +{ + "schema": 1, + "checked": "2026-10-17", + "documentation_checked": "2026-09", + "body_sha256": "1f578874874b2d4b757f649adda8745f4d241949ddc0c71bfffb7315a7158257", + "components": { + "le": { + "name": "Let's Encrypt documentation", + "basis": "unknown", + "sources": { + "s9ff2d787c618": "https://letsencrypt.org/docs/", + "s01388c85c5d6": "https://letsencrypt.org/docs/cert-lifetimes/", + "s4406ef210fff": "https://letsencrypt.org/2025/12/02/from-90-to-45", + "s21efa5995c48": "https://letsencrypt.org/2025/08/06/ocsp-service-has-reached-end-of-life", + "saf908bcd2143": "https://letsencrypt.org/docs/caa/" + } + }, + "zerossl": { + "name": "ZeroSSL", + "basis": "unknown", + "sources": { + "sddca28a3381b": "https://zerossl.com/" + } + }, + "certbot": { + "name": "Certbot documentation", + "basis": "unknown", + "sources": { + "s44faaf4eb7d7": "https://certbot.eff.org/", + "s7b60a00cadf3": "https://eff-certbot.readthedocs.io/en/stable/using.html#manual", + "s875be163b4c1": "https://eff-certbot.readthedocs.io/en/stable/using.html#dns-plugins" + } + }, + "reconfigure": { + "name": "Certbot reconfigure minimum", + "basis": "2.3.0", + "sources": { + "s0dee7d6d65ba": "https://raw.githubusercontent.com/certbot/certbot/v2.3.0/certbot/docs/using.rst" + } + }, + "acme": { + "name": "acme.sh", + "basis": "unknown", + "sources": { + "sa635c50fc1f9": "https://github.com/acmesh-official/acme.sh" + } + }, + "certbot-v230": { + "name": "Certbot user guide and source", + "basis": "v2.3.0", + "sources": { + "s725109195132": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L808-L825", + "s2f25bc233392": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L632-L640", + "s11c7994242fc": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L704-L710", + "sbb0c60d635ee": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L684", + "s55ec19f37395": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L194-L220", + "s5bf4ff9fbe76": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L222-L265", + "s7a053488f705": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/cli_utils.py#L227-L231", + "sa18c858c4e49": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/renewal.py#L40-L53", + "s2e4bd79a7795": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/renewal.py#L509-L534", + "s1910987bfd8c": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L295-L312", + "s9c6118b72e0a": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L1120-L1128", + "s3e22e32a2862": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L1243-L1249" + } + }, + "certbot-dns-cloudflare-v230": { + "name": "Certbot Cloudflare DNS plugin", + "basis": "v2.3.0", + "sources": { + "s3f07ef3d544d": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py#L24-L39", + "sc3b33fa2c90f": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py#L61-L78" + } + }, + "caddy": { + "name": "Caddy automatic HTTPS", + "basis": "v2.11.4", + "sources": { + "s9683cb8d689f": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L62-L63", + "s409f9221aec2": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L280-L296", + "s80dbf0751153": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L245-L249", + "s5216789c9341": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L321-L328" + } + }, + "certmagic": { + "name": "certmagic ACME implementation", + "basis": "v0.25.3", + "sources": { + "s61e3cba719df": "https://github.com/caddyserver/caddy/blob/v2.11.4/go.mod#L12-L12", + "s8782e006523b": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeclient.go#L188-L226", + "s465c4ddcc92f": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeclient.go#L304-L313", + "sc1e2ddefe5ed": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L116-L120", + "sd423f53faa1a": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L222-L236", + "sf2fbc79f6542": "https://github.com/caddyserver/certmagic/blob/v0.25.3/certmagic.go#L473-L475", + "s1c3236df58b9": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L647-L654" + } + }, + "traefik": { + "name": "Traefik ACME documentation", + "basis": "v3.5", + "sources": { + "s418a6b1ad09b": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/tls/certificate-resolvers/acme/" + } + }, + "cloudflare-origin-ca": { + "name": "Cloudflare Origin CA documentation", + "basis": "checked October 2026", + "sources": { + "s1e15de5c9f36": "https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/" + } + }, + "openssl-cli": { + "name": "OpenSSL certificate commands", + "basis": "3.0", + "sources": { + "s00aaf106164f": "https://docs.openssl.org/3.0/man1/openssl-s_client/", + "s0856f4a58574": "https://docs.openssl.org/3.0/man1/openssl-x509/" + } + }, + "curl-source": { + "name": "curl command documentation", + "basis": "curl-8_12_1", + "sources": { + "s76f69702c9d5": "https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/head.md#L21-L22", + "s60820cd231e1": "https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/insecure.md#L21-L28" + } + } + }, + "claims": { + "public-trust": {"text": "ACME CAs such as Let's Encrypt and ZeroSSL issue free publicly trusted certificates; this guide advises preferring them for public DNS names, including private hosts using DNS-01.", "components": ["le", "zerossl"], "sources": ["le:s9ff2d787c618", "zerossl:sddca28a3381b"], "status": "REASONED"}, + "http-challenge": {"text": "HTTP-01 needs a public A/AAAA or CNAME pointing to the server and inbound internet access to port 80.", "components": ["le"], "sources": ["le:s9ff2d787c618"], "status": "REASONED"}, + "alpn-challenge": {"text": "TLS-ALPN-01 needs a public DNS record and inbound port 443; Caddy's ACME issuer enables the TLS-ALPN-01 solver when DNS-01 is not configured and TLS-ALPN-01 is not disabled, and can use an alternate local port if port 443 forwards to it; Traefik also uses TLS-ALPN-01.", "components": ["le", "caddy", "certmagic", "traefik"], "sources": ["le:s9ff2d787c618", "caddy:s409f9221aec2", "caddy:s80dbf0751153", "caddy:s5216789c9341", "certmagic:s61e3cba719df", "certmagic:s8782e006523b", "certmagic:s465c4ddcc92f", "certmagic:sc1e2ddefe5ed", "certmagic:sd423f53faa1a", "certmagic:sf2fbc79f6542", "certmagic:s1c3236df58b9", "traefik:s418a6b1ad09b"], "status": "REASONED"}, + "dns-challenge": {"text": "DNS-01 needs control of the challenge TXT record, no public address record or inbound port, and supports wildcards; automated issuance and renewal need an automated way to update TXT records, and this guide advises a provider DNS plugin and scoped API credentials, while Certbot also supports manual DNS-01 and an authentication hook for automatic renewal.", "components": ["le", "certbot", "certbot-v230"], "sources": ["le:s9ff2d787c618", "certbot:s7b60a00cadf3", "certbot:s875be163b4c1", "certbot-v230:s55ec19f37395", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"}, + "certbot-install": {"text": "Install Certbot and nginx/Apache plugins from the distribution, or the classic snap with /usr/bin/certbot pointing to /snap/bin/certbot.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "server-plugins": {"text": "certbot --nginx or --apache issues and installs certificates for the supplied domain names when the server is supported.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "standalone": {"text": "certonly --standalone binds port 80; stop its current owner and arrange webroot, a server plugin or port-freeing hooks for unattended renewal.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "webroot": {"text": "certonly --webroot uses /var/www/html to serve challenge files while the existing web server keeps running.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "dns-credentials": {"text": "The Cloudflare DNS plugin uses a credentials file and a Zone:DNS:Edit token limited to required zones; this guide requires never using the Global API Key and protecting the file with mode 600 in a 700 directory.", "components": ["certbot", "certbot-dns-cloudflare-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-dns-cloudflare-v230:s3f07ef3d544d", "certbot-dns-cloudflare-v230:sc3b33fa2c90f"], "status": "REASONED"}, + "certificate-paths": {"text": "Configure servers against stable /etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem paths for the chain and private key.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "renewal-schedule": {"text": "The Certbot v2.3.0 user guide says most installations have a scheduled task running certbot renew and says that, on Linux and BSD, you can check cron or systemd timers if unsure; this guide requires confirming the task and testing renewal plus reload with --dry-run --run-deploy-hooks, including package and snap installations.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-v230:s725109195132"], "status": "REASONED"}, + "deploy-hook": {"text": "This guide requires persisting a reload hook during successful initial issuance or successful renewal, or installing an executable in renewal-hooks/deploy; Certbot v2.3.0 saves the renewal-relevant options set on the command line to a lineage after successful renewal, and a renew --deploy-hook invocation with no renewal due does not save that hook.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-v230:s2f25bc233392", "certbot-v230:s11c7994242fc", "certbot-v230:sbb0c60d635ee", "certbot-v230:s7a053488f705", "certbot-v230:sa18c858c4e49", "certbot-v230:s2e4bd79a7795", "certbot-v230:s1910987bfd8c", "certbot-v230:s9c6118b72e0a", "certbot-v230:s3e22e32a2862"], "status": "REASONED"}, + "reconfigure": {"text": "certbot reconfigure can add the reload hook later on Certbot 2.3.0 and later.", "components": ["reconfigure"], "sources": ["reconfigure:s0dee7d6d65ba"], "status": "REASONED"}, + "expiry-monitoring": {"text": "Monitor the served certificate's expiry and renewal failures instead of relying on CA reminder emails; include a dry run in the deployment checklist. This guide assesses that any renewal step involving a person will fail at those scheduled shorter certificate lifetimes and that unattended automation is the only viable path.", "components": ["le", "certbot"], "sources": ["le:s4406ef210fff", "certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "default-lifetime": {"text": "At the guide's September 2026 documentation check, Let's Encrypt's default certificate lifetime is 90 days.", "components": ["le"], "sources": ["le:s01388c85c5d6"], "status": "REASONED"}, + "short-lifetime": {"text": "Optional 6-day short-lived certificates are available to every subscriber as of the guide's September 2026 check.", "components": ["le"], "sources": ["le:s01388c85c5d6"], "status": "REASONED"}, + "lifetime-schedule": {"text": "The classic profile moves to 64 days on 2027-02-10 and 45 days on 2028-02-16; industry rules cap publicly trusted certificates at 47 days from 2029-03-15.", "components": ["le"], "sources": ["le:s01388c85c5d6", "le:s4406ef210fff"], "status": "REASONED"}, + "revocation": {"text": "Let's Encrypt ended OCSP on 2025-08-06 and publishes revocation through CRLs only; do not add OCSP stapling directives for its certificates.", "components": ["le"], "sources": ["le:s21efa5995c48"], "status": "REASONED"}, + "staging": {"text": "Let's Encrypt limits per-domain issuance; test with certbot --staging or --test-cert before real issuance.", "components": ["le", "certbot"], "sources": ["le:s9ff2d787c618", "certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "caa": {"text": "CAA restricts issuing CAs, using issue letsencrypt.org for Let's Encrypt; certificate-transparency monitoring detects mis-issuance afterwards and complements CAA.", "components": ["le"], "sources": ["le:saf908bcd2143", "le:s9ff2d787c618"], "status": "REASONED"}, + "zerossl-eab": {"text": "ZeroSSL offers free ACME certificates; some clients need dashboard EAB credentials.", "components": ["zerossl"], "sources": ["zerossl:sddca28a3381b"], "status": "REASONED"}, + "acme-default": {"text": "acme.sh registers with ZeroSSL by default.", "components": ["acme"], "sources": ["acme:sa635c50fc1f9"], "status": "REASONED"}, + "acme-install": {"text": "acme.sh is a shell ACME client supporting many DNS providers; the guide recommends installation from a repository release instead of piping a download into a shell.", "components": ["acme"], "sources": ["acme:sa635c50fc1f9"], "status": "REASONED"}, + "server-automation": {"text": "This guide advises Caddy or Traefik for built-in issuance and renewal without an external client; it assesses them as the lowest-effort correct option for new deployments.", "components": ["le", "caddy", "traefik"], "sources": ["le:s9ff2d787c618", "caddy:s9683cb8d689f", "traefik:s418a6b1ad09b"], "status": "REASONED"}, + "origin-certificates": {"text": "Cloudflare lists Origin CA certificates on its Free plan, refers to long-lived origin CA certificates and says the validity period is chosen at creation, says they only encrypt traffic between Cloudflare and the origin, and warns that visitors may see untrusted certificate errors when Cloudflare is paused or proxying is disabled; this guide requires using them only for origins that receive traffic only through Cloudflare-proxied records.", "components": ["le", "cloudflare-origin-ca"], "sources": ["le:s9ff2d787c618", "cloudflare-origin-ca:s1e15de5c9f36"], "status": "REASONED"}, + "verify-inventory": {"text": "certbot certificates lists issued certificates and their expiry dates.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED", "verify": [1]}, + "verify-https": {"text": "Use the deployment's public hostname rather than example.com; curl HTTPS should succeed without -k.", "components": ["le", "curl-source"], "sources": ["le:s9ff2d787c618", "curl-source:s76f69702c9d5", "curl-source:s60820cd231e1"], "status": "REASONED", "verify": [1]}, + "verify-certificate": {"text": "The OpenSSL probe targets the hostname on port 443 with SNI, hostname verification and verification errors enabled, then displays issuer and dates.", "components": ["le", "openssl-cli"], "sources": ["le:s9ff2d787c618", "openssl-cli:s00aaf106164f", "openssl-cli:s0856f4a58574"], "status": "REASONED", "verify": [1]}, + "authentication": {"text": "A certificate alone is insufficient protection; continue with server controls and authentication.", "components": ["le"], "sources": ["le:s9ff2d787c618"], "status": "REASONED"}, + "dns-automation": {"text": "For automated DNS-01 issuance and renewal, this guide advises using a provider DNS plugin and least-privilege API credentials; Certbot also documents manual TXT entry and authentication hooks that automate the challenge.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s875be163b4c1", "certbot:s7b60a00cadf3", "certbot-v230:s55ec19f37395", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"}, + "dns-manual": {"text": "certbot certonly --manual --preferred-challenges dns permits hand-created TXT records; the Certbot v2.3.0 guide says certificates created with --manual do not support automatic renewal without a --manual-auth-hook that automates the challenge.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s7b60a00cadf3", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"} + } +} +```` + diff --git a/gitops-controllers.md b/gitops-controllers.md index fdef731a6..0bdc1676b 100644 --- a/gitops-controllers.md +++ b/gitops-controllers.md @@ -1,318 +1,52 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-19", - "documentation_checked": "2026-09", - "body_sha256": "908b108054ff323b7d30f6ec6652728a0d065571804b124943b579b8f09f77cc", - "components": { - "argo": { - "name": "Argo CD v3.5.3 source", - "basis": "c9c369efcc5b2a0bd720803f8d14a1c3eaddf579", - "sources": { - "sddf4853ad503": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-service.yaml#L9-L20", - "s6e8c96d23e05": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-server/commands/argocd_server.go#L307-L323", - "s96065cc09776": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/server.go#L512-L535", - "s7cc690d1bf69": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/server.go#L668-L677", - "s028a4769d6c3": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/metrics/metrics.go#L74-L100", - "sc52bbd14bff4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/profile/profile.go#L11-L30", - "s98b27313f615": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-repo-server/commands/argocd_repo_server.go#L180-L212", - "sb0648e746fb9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L406-L408", - "s2f5af86c62d1": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L475-L481", - "s9bca471ac6d9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-repo-server/commands/argocd_repo_server.go#L257-L280", - "s4bbc848d9923": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/repo-server/argocd-repo-server-deployment.yaml#L367-L384", - "sf98b6bc4f5dc": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/application-controller/argocd-application-controller-statefulset.yaml#L360-L365", - "sf4dd846c82c4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/common/common.go#L75-L92", - "s49a7c83d9371": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-applicationset-controller/commands/applicationset_controller.go#L294-L296", - "s97a588d8a83f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/applicationset-controller/argocd-applicationset-controller-service.yaml#L9-L20", - "s0a85b38dfca8": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-notification/commands/argocd_notification.go#L149-L154", - "s5e141e32ed9c": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/notification/argocd-notifications-controller-metrics-service.yaml#L9-L16", - "s8efcc66f9cb4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-dex/commands/argocd_dex.go#L82-L119", - "s30c391d9dc23": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-dex/commands/argocd_dex.go#L143-L146", - "se477de2fdfa0": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/dex/config.go#L16-L152", - "s56c41c02ba8b": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-service.yaml#L9-L25", - "s996c72c5db28": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/redis/argocd-redis-deployment.yaml#L18-L58", - "s34909d7a1932": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/cache/cache.go#L233-L238", - "s1a13cc06db6d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/repo-server/argocd-repo-server-network-policy.yaml#L9-L35", - "s4bbf38e83770": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/redis/argocd-redis-network-policy.yaml#L9-L28", - "sadccbbcffcd8": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-network-policy.yaml#L9-L29", - "s743c8187cbd7": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-network-policy.yaml#L9-L16", - "s49f63c21c3cd": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/application-controller/argocd-application-controller-network-policy.yaml#L9-L19", - "sa258c4109d24": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/applicationset-controller/argocd-applicationset-controller-network-policy.yaml#L9-L22", - "sa49c30e332ed": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/notification/argocd-notifications-controller-network-policy.yaml#L9-L20", - "s68b67007da6b": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/settings/settings.go#L1631-L1638", - "s01a52daf078f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L121-L126", - "s210b24f04535": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/config/argocd-cmd-params-cm.yaml#L1-L7", - "s0f7f1b3404d9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/config/argocd-rbac-cm.yaml#L1-L7", - "s85445da68ce7": "https://github.com/argoproj/argo-cd/blob/v3.5.3/common/common.go#L89-L90", - "s357d253fc789": "https://github.com/argoproj/argo-cd/blob/v3.5.3/cmd/argocd-repo-server/commands/argocd_repo_server.go#L257-L260", - "s9df1243dde48": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-application-controller/commands/argocd_application_controller.go#L192-L209", - "s0abbc26d61e4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-application-controller/commands/argocd_application_controller.go#L272-L273", - "sfcaf2e03154d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/reposerver/server.go#L49-L85", - "s25a5da2c5e7c": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/reposerver/server.go#L94-L150", - "s73653423b85d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/tls/tls.go#L428-L506", - "s7c7bf274e3ef": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-deployment.yaml#L38-L42", - "s08939ffb906f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd/commands/admin/redis_initial_password.go#L22-L89", - "s3fcdf1601776": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L454-L467", - "se987196f20c7": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L539-L547", - "s3cb98aa5348d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L399-L425", - "s9bfcf07a6bcc": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L314-L346", - "s32323471d785": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L35-L42" - } - }, - "dex": { - "name": "Dex v2.45.1 source", - "basis": "v2.45.1", - "sources": { - "s3b9512b79dea": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/serve.go#L151-L199", - "s46eb2feaa84f": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/serve.go#L553-L572", - "s55b453f266f3": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/config.go#L255-L265" - } - }, - "redis-image": { - "name": "docker-library-redis v8.2.3 source", - "basis": "bcbc692165ce2241e194035041c5b5e01d2e1a5f", - "sources": { - "s67a2f7553c9a": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/Dockerfile#L81-L87", - "s30643c91686d": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/Dockerfile#L151-L154", - "sd04db8453288": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/docker-entrypoint.sh#L93-L111" - } - }, - "redis-source": { - "name": "Redis 8.2.3 source", - "basis": "8.2.3", - "sources": { - "s667fc16bf18f": "https://github.com/redis/redis/blob/8.2.3/src/server.h#L142-L143", - "saa497d27f72f": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2199-L2221", - "s0f96763525b7": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2638-L2673", - "s9b50a7c52721": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2983-L2996" - } - }, - "argo-docs": { - "name": "Argo CD documentation", - "basis": "unknown", - "sources": { - "s51e21b74ece6": "https://argo-cd.readthedocs.io/en/stable/operator-manual/security/", - "s02463a4fb268": "https://argo-cd.readthedocs.io/en/stable/operator-manual/secret-management/", - "s2efb9ee633af": "https://argo-cd.readthedocs.io/en/stable/getting_started/", - "sc2590df86f4e": "https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/", - "sf5f1c41f89bd": "https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/", - "s3767d400e364": "https://argo-cd.readthedocs.io/en/stable/operator-manual/ingress/", - "sdac90731b97d": "https://argo-cd.readthedocs.io/en/stable/operator-manual/webhook/", - "s9357e3b59e84": "https://argo-cd.readthedocs.io/en/stable/user-guide/projects/" - } - }, - "curl": { - "name": "curl", - "basis": "curl-8_14_1", - "sources": { - "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md", - "s9537bb790553": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data-binary.md#L4-L32", - "sd3b039bafbf6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data.md#L4-L49" - } - }, - "flux": { - "name": "Flux documentation", - "basis": "unknown", - "sources": { - "s493859f3b848": "https://fluxcd.io/flux/security/best-practices/", - "s4dcd744a2444": "https://fluxcd.io/flux/guides/mozilla-sops/", - "sd8618bbbc628": "https://fluxcd.io/flux/components/source/ocirepositories/#secret-reference", - "sf3f80d9941c8": "https://fluxcd.io/flux/installation/configuration/multitenancy/", - "s0669b973542c": "https://fluxcd.io/flux/security/#controller-permissions", - "s13bd7f0204b0": "https://fluxcd.io/flux/installation/bootstrap/github/", - "s8759854c1bdf": "https://fluxcd.io/flux/components/image/imageupdateautomations/", - "s0acf42beed45": "https://fluxcd.io/flux/guides/webhook-receivers/", - "s35bb9a844dcf": "https://fluxcd.io/flux/components/notification/receivers/" - } - }, - "flux-oidc": { - "name": "Flux OIDC introduction", - "basis": "v2.9", - "sources": { - "s1ea193b2b3cc": "https://fluxcd.io/blog/2026/06/flux-v2.9.0/" - } - }, - "python": { - "name": "Python documentation", - "basis": "unknown", - "sources": { - "s7a54556e5dac": "https://docs.python.org/3/library/hmac.html", - "s31fccd7d50e5": "https://docs.python.org/3/library/sys.html#sys.stdin" - } - } - }, - "claims": { - "git-boundary": {"text": "Repository writers exercise reconciliation permissions; review platform changes, separate tenants and scope credentials.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s51e21b74ece6", "flux:s493859f3b848"], "status": "REASONED"}, - "secret-storage": {"text": "Protect bootstrap, registry, cluster, webhook and decryption credentials; commit only encrypted secrets and keep SOPS/age keys out of Git.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s02463a4fb268", "flux:s4dcd744a2444", "flux:sd8618bbbc628"], "status": "REASONED"}, - "egress": {"text": "Restrict fetch, API, identity and notification egress; Argo repository allowlists do not constrain remote bases or Helm dependencies.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s51e21b74ece6", "flux:sf3f80d9941c8"], "status": "REASONED"}, - "argo-bootstrap": {"text": "Rotate the initial admin password before deleting argocd-initial-admin-secret; deletion alone does not rotate it.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:sc2590df86f4e"], "status": "REASONED"}, - "argo-sso": {"text": "The built-in admin is unrestricted; configure named SSO roles and IdP MFA, then set admin.enabled=false.", "components": ["argo-docs"], "sources": ["argo-docs:sc2590df86f4e", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, - "argo-origin": {"text": "Keep the ClusterIP origin private behind authenticated TLS and protect both REST/browser and CLI/gRPC routes.", "components": ["argo-docs", "argo"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:s3767d400e364", "argo:sddf4853ad503"], "status": "REASONED"}, - "argo-server": {"text": "Server defaults to 0.0.0.0:8080; the Service maps 80 and 443 to it.", "components": ["argo"], "sources": ["argo:s6e8c96d23e05", "argo:sddf4853ad503", "argo:sf4dd846c82c4"], "status": "REASONED"}, - "argo-tls": {"text": "Server --insecure/server.insecure defaults false and disables backend TLS, not authentication; use only behind protected TLS termination.", "components": ["argo", "argo-docs"], "sources": ["argo:s6e8c96d23e05", "argo-docs:s3767d400e364"], "status": "REASONED"}, - "argo-client-tls": {"text": "argocd login --insecure skips client certificate verification; the server initially uses a self-signed certificate.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:s3767d400e364"], "status": "REASONED"}, - "argo-metrics": {"text": "Server HTTP metrics on 8083 are unauthenticated and bind to --address, not --metrics-address.", "components": ["argo"], "sources": ["argo:s96065cc09776", "argo:s7cc690d1bf69", "argo:s028a4769d6c3", "argo:sf4dd846c82c4", "argo:s6e8c96d23e05"], "status": "REASONED"}, - "argo-profiler": {"text": "Server and repo-server pprof paths return 401 unless the profiler file contains exactly true with no newline; enabled profiling has no caller authentication.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:s028a4769d6c3", "argo:s98b27313f615"], "status": "REASONED"}, - "argo-profiler-file": {"text": "ARGOCD_ENABLE_PROFILER_FILE_PATH defaults to /home/argocd/params/profiler.enabled; the server mounts ConfigMap key server.profile.enabled there.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:sb0648e746fb9", "argo:s2f5af86c62d1"], "status": "REASONED"}, - "repo-grpc": {"text": "Repo-server gRPC binds 0.0.0.0:8081 with TLS by default; mTLS is skipped without the client CA, separate from the serving certificate.", "components": ["argo"], "sources": ["argo:s9bca471ac6d9", "argo:s4bbc848d9923", "argo:sf4dd846c82c4", "argo:s85445da68ce7", "argo:s357d253fc789"], "status": "REASONED"}, - "repo-http": {"text": "Repo-server HTTP metrics and health bind 0.0.0.0:8084 without an authentication wrapper.", "components": ["argo"], "sources": ["argo:s98b27313f615", "argo:s9bca471ac6d9", "argo:sf4dd846c82c4", "argo:s85445da68ce7", "argo:s357d253fc789"], "status": "REASONED"}, - "repo-limits": {"text": "Repo-server requires verified client certificates with a loaded client CA; missing or unstatable serving files trigger a session self-signed certificate, while an invalid present pair errors. No authorization interceptor appears; service method authorization is unverified.", "components": ["argo"], "sources": ["argo:sfcaf2e03154d", "argo:s25a5da2c5e7c", "argo:s73653423b85d"], "status": "REASONED"}, - "controller-metrics": {"text": "Application-controller metrics and health use 8082; the command passes that port to the controller, but the fetched files do not establish the bind address.", "components": ["argo"], "sources": ["argo:sf98b6bc4f5dc", "argo:s9df1243dde48", "argo:s0abbc26d61e4", "argo:sf4dd846c82c4"], "status": "REASONED"}, - "applicationset-webhook": {"text": "ApplicationSet webhook binds :7000; its container and Service declare 7000.", "components": ["argo"], "sources": ["argo:s49a7c83d9371", "argo:s97a588d8a83f"], "status": "REASONED"}, - "applicationset-metrics": {"text": "ApplicationSet metrics bind :8080; its container and Service declare 8080.", "components": ["argo"], "sources": ["argo:s49a7c83d9371", "argo:s97a588d8a83f"], "status": "REASONED"}, - "applicationset-health": {"text": "ApplicationSet health probes bind :8081 without a container or Service port declaration; missing declarations do not close sockets.", "components": ["argo"], "sources": ["argo:s49a7c83d9371"], "status": "REASONED"}, - "notifications": {"text": "Notifications HTTP metrics bind 0.0.0.0:9001 without authentication; a metrics Service and TCP liveness probe exist without containerPort.", "components": ["argo"], "sources": ["argo:s0a85b38dfca8", "argo:s5e141e32ed9c"], "status": "REASONED"}, - "dex-http": {"text": "Dex starts only with nonempty generated configuration; HTTP defaults to TLS on 0.0.0.0:5556 using /tmp/tls.crt and /tmp/tls.key.", "components": ["argo"], "sources": ["argo:s8efcc66f9cb4", "argo:s30c391d9dc23", "argo:se477de2fdfa0"], "status": "REASONED"}, - "dex-grpc": {"text": "Generated Dex gRPC uses 0.0.0.0:5557 without TLS or client-auth settings; Dex v2.45.1 source sets no transport credentials or authentication interceptor for this configuration, while API method authorization is unverified; this assumes the deployed ghcr.io/dexidp/dex:v2.45.1 tag was built from Dex v2.45.1.", "components": ["argo", "dex"], "sources": ["argo:se477de2fdfa0", "argo:s56c41c02ba8b", "argo:s7c7bf274e3ef", "dex:s3b9512b79dea", "dex:s46eb2feaa84f", "dex:s55b453f266f3"], "status": "REASONED"}, - "dex-telemetry": {"text": "Dex telemetry uses HTTP on 0.0.0.0:5558; all three Dex ports have Services.", "components": ["argo"], "sources": ["argo:se477de2fdfa0", "argo:s56c41c02ba8b"], "status": "REASONED"}, - "redis-password": {"text": "Redis receives --requirepass from argocd-redis/auth; secret-init generates and checks that Secret. The redis:8.2.3-alpine tag, taken as built from docker-library-redis v8.2.3, uses Redis wildcard bind defaults and patches protected mode off.", "components": ["argo", "redis-image", "redis-source"], "sources": ["argo:s996c72c5db28", "argo:s08939ffb906f", "redis-image:s67a2f7553c9a", "redis-image:s30643c91686d", "redis-image:sd04db8453288", "redis-source:s667fc16bf18f", "redis-source:saa497d27f72f", "redis-source:s0f96763525b7", "redis-source:s9b50a7c52721"], "status": "REASONED"}, - "redis-tls": {"text": "Base manifests configure no Redis TLS and the Argo Redis client defaults --redis-use-tls=false; protect traffic, Secret and cache.", "components": ["argo"], "sources": ["argo:s996c72c5db28", "argo:s34909d7a1932"], "status": "REASONED"}, - "component-policies": {"text": "Base policies restrict repo gRPC, Redis and Dex callers by component; enforcement needs a supporting CNI.", "components": ["argo"], "sources": ["argo:s1a13cc06db6d", "argo:s4bbf38e83770", "argo:sadccbbcffcd8"], "status": "REASONED"}, - "broad-policies": {"text": "Server policy allows all ingress; other metrics and ApplicationSet webhook allowances admit every namespace. Narrow existing additive allowances and egress separately.", "components": ["argo"], "sources": ["argo:s743c8187cbd7", "argo:s49f63c21c3cd", "argo:sa258c4109d24", "argo:sa49c30e332ed"], "status": "REASONED"}, - "argo-anonymous": {"text": "users.anonymous.enabled defaults off; anonymous callers otherwise inherit policy.default.", "components": ["argo", "argo-docs"], "sources": ["argo:s68b67007da6b", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, - "argo-disable-auth": {"text": "server.disable.auth defaults false; inspect effective args and environment because anonymous-off does not compensate for disabled authentication.", "components": ["argo"], "sources": ["argo:s6e8c96d23e05", "argo:s01a52daf078f", "argo:s210b24f04535"], "status": "REASONED"}, - "argo-default-role": {"text": "Base RBAC ConfigMap omits policy.default; the inspected loader supplies an empty role to the enforcer, which skips its additive default-role check. Default-role grants are additive and cannot be revoked by a later subject rule; define scoped policy.csv roles and group mappings. Server-side wiring remains unverified.", "components": ["argo", "argo-docs"], "sources": ["argo:s0f7f1b3404d9", "argo:s3fcdf1601776", "argo:se987196f20c7", "argo:s3cb98aa5348d", "argo:s9bfcf07a6bcc", "argo:s32323471d785", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, - "argo-webhook": {"text": "/api/webhook accepts unauthenticated refresh events without a shared secret; configure one to limit spoofed reconciliation and resource exhaustion.", "components": ["argo-docs"], "sources": ["argo-docs:sdac90731b97d"], "status": "REASONED"}, - "argo-projects": {"text": "Restrict AppProjects including the built-in default project; project constraints do not reduce a compromised controller's Kubernetes credentials.", "components": ["argo-docs"], "sources": ["argo-docs:s9357e3b59e84", "argo-docs:s51e21b74ece6"], "status": "REASONED"}, - "argo-cluster-rbac": {"text": "Reduce controller and registered-cluster argocd-manager write permissions to needed namespaces/resources while retaining required read access.", "components": ["argo-docs"], "sources": ["argo-docs:s51e21b74ece6"], "status": "REASONED"}, - "flux-api": {"text": "Flux has no standalone management login/API; Kubernetes authenticates its custom-resource operations, while separate dashboards need their own controls.", "components": ["flux"], "sources": ["flux:s493859f3b848", "flux:s0669b973542c"], "status": "REASONED"}, - "flux-bootstrap": {"text": "GitHub bootstrap --token-auth retains a PAT in flux-system; --token-auth=false creates a read-only SSH deploy key by default. Protect bootstrap input and Secrets.", "components": ["flux"], "sources": ["flux:s13bd7f0204b0"], "status": "REASONED"}, - "flux-writeback": {"text": "Image automation needs Git write access, including --read-write-key=true for the bootstrap key; scope the repository and protect update branches and objects.", "components": ["flux"], "sources": ["flux:s13bd7f0204b0", "flux:s8759854c1bdf"], "status": "REASONED"}, - "flux-artifacts": {"text": "Source-controller serves fetched artifacts over internal HTTP; enforced NetworkPolicy, not ClusterIP alone, isolates them.", "components": ["flux"], "sources": ["flux:s493859f3b848"], "status": "REASONED"}, - "flux-receiver-port": {"text": "Webhook receiver listens on 9292 behind Service port 80; external exposure needs a route and TLS termination.", "components": ["flux"], "sources": ["flux:s0acf42beed45"], "status": "REASONED"}, - "flux-receiver-types": {"text": "generic validates nothing; generic-hmac, GitHub, Bitbucket and Nexus use HMAC, while gitlab compares X-Gitlab-Token.", "components": ["flux"], "sources": ["flux:s35bb9a844dcf"], "status": "REASONED"}, - "flux-oidc": {"text": "generic-oidc, introduced in Flux 2.9, rejects secretRef and validates bearer tokens; constrain issuer validations and audience, whose default is notification-controller.", "components": ["flux", "flux-oidc"], "sources": ["flux:s35bb9a844dcf", "flux-oidc:s1ea193b2b3cc"], "status": "REASONED"}, - "flux-rbac": {"text": "Only kustomize-controller and helm-controller hold cluster-admin; other controllers still have meaningful permissions including Secret access.", "components": ["flux"], "sources": ["flux:s0669b973542c"], "status": "REASONED"}, - "flux-lockdown": {"text": "Use scoped reconciliation ServiceAccounts, --no-cross-namespace-refs and --no-remote-bases; --default-service-account fills an omission, not an explicit serviceAccountName.", "components": ["flux"], "sources": ["flux:sf3f80d9941c8"], "status": "REASONED"}, - "verify-argo": {"text": "Anonymous application listing should fail with 401 while a valid token lists the expected application; repeat at origin and CLI/gRPC and inspect effective RBAC.", "components": ["argo-docs", "argo", "curl"], "sources": ["argo-docs:sf5f1c41f89bd", "argo:s6e8c96d23e05", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, - "verify-bootstrap": {"text": "Absence of the bootstrap Secret proves removal of that material, not password rotation.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af"], "status": "REASONED", "verify": [1]}, - "verify-inventory": {"text": "Inventory Services, routes, host ports, policies and component sockets; node-local ss and policy objects alone do not establish cluster isolation.", "components": ["argo", "flux"], "sources": ["argo:s743c8187cbd7", "flux:s493859f3b848"], "status": "REASONED", "verify": [1]}, - "verify-repo-metrics": {"text": "Base policy permits HTTP 200 metrics to monitoring and unrelated pods; narrowed policy should admit only monitoring, with a successful positive control.", "components": ["argo"], "sources": ["argo:s98b27313f615", "argo:s1a13cc06db6d"], "status": "REASONED", "verify": [2]}, - "verify-server-metrics": {"text": "Repeat on server 8083: only authorized monitoring should receive metrics after narrowing ingress; a failed control is inconclusive.", "components": ["argo"], "sources": ["argo:s028a4769d6c3", "argo:s743c8187cbd7"], "status": "REASONED"}, - "verify-profiler": {"text": "From an allowed pod, pprof cmdline should return 401 disabled, 200 for exact true, then 401 for true plus newline; keep metrics as control.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:s028a4769d6c3"], "status": "REASONED"}, - "verify-flux": {"text": "Identical generic-hmac bodies without and with X-Signature should be rejected and accepted with reconciliation; status is type/version dependent. The cited curl curl-8_14_1 lines show --data-binary posting data exactly as specified, preserving newlines and carriage returns that --data strips from @file input, reading @- from stdin and @filename from a file, and posting like --data's POST request.", "components": ["flux", "python", "curl"], "sources": ["flux:s35bb9a844dcf", "python:s7a54556e5dac", "python:s31fccd7d50e5", "curl:s3db522258e44", "curl:s9537bb790553", "curl:sd3b039bafbf6"], "status": "REASONED", "verify": [3]}, - "verify-receiver-status": {"text": "Read receiver name and Ready condition without exposing webhookPath or condition messages; origin checks distinguish receiver rejection from proxy rejection.", "components": ["flux"], "sources": ["flux:s35bb9a844dcf"], "status": "REASONED"} - } -} ---- # GitOps controllers: Argo CD and Flux - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +Written against Argo CD v3.5.3 source (pinned to a commit), Flux OIDC introduction v2.9, Dex v2.45.1 source v2.45.1, Redis 8.2.3 source 8.2.3, and curl curl-8_14_1; 1 further source is pinned to a commit and 3 carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 50 claims, all reasoned from cited sources. -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. -Components (recorded basis): +Argo CD and Flux reconcile a git repository into a Kubernetes cluster, so both hold +cluster-admin-equivalent power: whoever can drive the controller, or change what it reconciles, can +deploy anything it can reach, across the clusters it is registered against and to the extent the controller's Kubernetes RBAC, its registered-cluster credentials, and project restrictions allow. Three exposures follow +from that and apply to both. The blast radius is the cluster (or clusters), so administrative access +deserves the same care as the Kubernetes API itself ([kubernetes.md](kubernetes.md)). Git is an +authorization boundary: whoever can merge to a reconciled repository, or redirect a source, exercises +the controller's permissions, so platform repositories need reviewed changes and separation from +tenant repositories. And the credentials the controllers hold (git deploy keys, cluster Secrets) +should be read-only and narrowly scoped where the work allows. Handle every credential class per +[secrets.md](secrets.md): bootstrap tokens, git and registry credentials (Flux `OCIRepository`/`HelmRepository` +`.spec.secretRef`), cluster credentials, webhook secrets, and decryption keys. Never commit plaintext +credentials or base64-only Secret manifests; encrypted SOPS files and SealedSecret resources may be +committed, but Sealed Secrets is a separate controller and SOPS/age decryption keys (Flux references them +from the Kustomization) must be protected and never committed. For Argo CD, prefer destination-cluster +secret operators, because config-management plugins can surface decrypted values through Redis and the +repo-server. The tools differ in one way that shapes +the rest: Argo CD runs a network-facing API and UI with its own accounts, while Flux has no standalone +management UI or user-login API of its own, and is driven through Kubernetes custom resources under the +API server's authentication and RBAC (its controllers still expose in-cluster HTTP endpoints, and a +separately installed Flux dashboard brings its own login and exposure to harden). Control-plane exposure itself, the API server and etcd, stays in +[kubernetes.md](kubernetes.md). Egress is a surface too: both controllers fetch from Git, container and +chart registries, the Kubernetes API, identity/KMS, and notification targets, and Argo CD's repository +allowlist does not constrain remote bases or Helm chart dependencies while its webhook path has an SSRF +consideration, so restrict controller egress to the destinations each deployment needs and apply the +metadata protections in [egress-metadata.md](egress-metadata.md), keeping only the metadata access the +identity mechanism requires (for Flux tenant reconciliation, `--no-remote-bases=true` where applicable). -- argo: Argo CD v3.5.3 source (c9c369efcc5b2a0bd720803f8d14a1c3eaddf579) -- dex: Dex v2.45.1 source (v2.45.1) -- redis-image: docker-library-redis v8.2.3 source (bcbc692165ce2241e194035041c5b5e01d2e1a5f) -- redis-source: Redis 8.2.3 source (8.2.3) -- argo-docs: Argo CD documentation (unknown) -- curl: curl (curl-8_14_1) -- flux: Flux documentation (unknown) -- flux-oidc: Flux OIDC introduction (v2.9) -- python: Python documentation (unknown) +## Argo CD -Claims (full source mapping in the front matter): - -- git-boundary (REASONED; argo-docs, flux): Repository writers exercise reconciliation permissions; review platform changes, separate tenants and scope credentials. -- secret-storage (REASONED; argo-docs, flux): Protect bootstrap, registry, cluster, webhook and decryption credentials; commit only encrypted secrets and keep SOPS/age keys out of Git. -- egress (REASONED; argo-docs, flux): Restrict fetch, API, identity and notification egress; Argo repository allowlists do not constrain remote bases or Helm dependencies. -- argo-bootstrap (REASONED; argo-docs): Rotate the initial admin password before deleting argocd-initial-admin-secret; deletion alone does not rotate it. -- argo-sso (REASONED; argo-docs): The built-in admin is unrestricted; configure named SSO roles and IdP MFA, then set admin.enabled=false. -- argo-origin (REASONED; argo-docs, argo): Keep the ClusterIP origin private behind authenticated TLS and protect both REST/browser and CLI/gRPC routes. -- argo-server (REASONED; argo): Server defaults to 0.0.0.0:8080; the Service maps 80 and 443 to it. -- argo-tls (REASONED; argo, argo-docs): Server --insecure/server.insecure defaults false and disables backend TLS, not authentication; use only behind protected TLS termination. -- argo-client-tls (REASONED; argo-docs): argocd login --insecure skips client certificate verification; the server initially uses a self-signed certificate. -- argo-metrics (REASONED; argo): Server HTTP metrics on 8083 are unauthenticated and bind to --address, not --metrics-address. -- argo-profiler (REASONED; argo): Server and repo-server pprof paths return 401 unless the profiler file contains exactly true with no newline; enabled profiling has no caller authentication. -- argo-profiler-file (REASONED; argo): ARGOCD_ENABLE_PROFILER_FILE_PATH defaults to /home/argocd/params/profiler.enabled; the server mounts ConfigMap key server.profile.enabled there. -- repo-grpc (REASONED; argo): Repo-server gRPC binds 0.0.0.0:8081 with TLS by default; mTLS is skipped without the client CA, separate from the serving certificate. -- repo-http (REASONED; argo): Repo-server HTTP metrics and health bind 0.0.0.0:8084 without an authentication wrapper. -- repo-limits (REASONED; argo): Repo-server requires verified client certificates with a loaded client CA; missing or unstatable serving files trigger a session self-signed certificate, while an invalid present pair errors. No authorization interceptor appears; service method authorization is unverified. -- controller-metrics (REASONED; argo): Application-controller metrics and health use 8082; the command passes that port to the controller, but the fetched files do not establish the bind address. -- applicationset-webhook (REASONED; argo): ApplicationSet webhook binds :7000; its container and Service declare 7000. -- applicationset-metrics (REASONED; argo): ApplicationSet metrics bind :8080; its container and Service declare 8080. -- applicationset-health (REASONED; argo): ApplicationSet health probes bind :8081 without a container or Service port declaration; missing declarations do not close sockets. -- notifications (REASONED; argo): Notifications HTTP metrics bind 0.0.0.0:9001 without authentication; a metrics Service and TCP liveness probe exist without containerPort. -- dex-http (REASONED; argo): Dex starts only with nonempty generated configuration; HTTP defaults to TLS on 0.0.0.0:5556 using /tmp/tls.crt and /tmp/tls.key. -- dex-grpc (REASONED; argo, dex): Generated Dex gRPC uses 0.0.0.0:5557 without TLS or client-auth settings; Dex v2.45.1 source sets no transport credentials or authentication interceptor for this configuration, while API method authorization is unverified; this assumes the deployed ghcr.io/dexidp/dex:v2.45.1 tag was built from Dex v2.45.1. -- dex-telemetry (REASONED; argo): Dex telemetry uses HTTP on 0.0.0.0:5558; all three Dex ports have Services. -- redis-password (REASONED; argo, redis-image, redis-source): Redis receives --requirepass from argocd-redis/auth; secret-init generates and checks that Secret. The redis:8.2.3-alpine tag, taken as built from docker-library-redis v8.2.3, uses Redis wildcard bind defaults and patches protected mode off. -- redis-tls (REASONED; argo): Base manifests configure no Redis TLS and the Argo Redis client defaults --redis-use-tls=false; protect traffic, Secret and cache. -- component-policies (REASONED; argo): Base policies restrict repo gRPC, Redis and Dex callers by component; enforcement needs a supporting CNI. -- broad-policies (REASONED; argo): Server policy allows all ingress; other metrics and ApplicationSet webhook allowances admit every namespace. Narrow existing additive allowances and egress separately. -- argo-anonymous (REASONED; argo, argo-docs): users.anonymous.enabled defaults off; anonymous callers otherwise inherit policy.default. -- argo-disable-auth (REASONED; argo): server.disable.auth defaults false; inspect effective args and environment because anonymous-off does not compensate for disabled authentication. -- argo-default-role (REASONED; argo, argo-docs): Base RBAC ConfigMap omits policy.default; the inspected loader supplies an empty role to the enforcer, which skips its additive default-role check. Default-role grants are additive and cannot be revoked by a later subject rule; define scoped policy.csv roles and group mappings. Server-side wiring remains unverified. -- argo-webhook (REASONED; argo-docs): /api/webhook accepts unauthenticated refresh events without a shared secret; configure one to limit spoofed reconciliation and resource exhaustion. -- argo-projects (REASONED; argo-docs): Restrict AppProjects including the built-in default project; project constraints do not reduce a compromised controller's Kubernetes credentials. -- argo-cluster-rbac (REASONED; argo-docs): Reduce controller and registered-cluster argocd-manager write permissions to needed namespaces/resources while retaining required read access. -- flux-api (REASONED; flux): Flux has no standalone management login/API; Kubernetes authenticates its custom-resource operations, while separate dashboards need their own controls. -- flux-bootstrap (REASONED; flux): GitHub bootstrap --token-auth retains a PAT in flux-system; --token-auth=false creates a read-only SSH deploy key by default. Protect bootstrap input and Secrets. -- flux-writeback (REASONED; flux): Image automation needs Git write access, including --read-write-key=true for the bootstrap key; scope the repository and protect update branches and objects. -- flux-artifacts (REASONED; flux): Source-controller serves fetched artifacts over internal HTTP; enforced NetworkPolicy, not ClusterIP alone, isolates them. -- flux-receiver-port (REASONED; flux): Webhook receiver listens on 9292 behind Service port 80; external exposure needs a route and TLS termination. -- flux-receiver-types (REASONED; flux): generic validates nothing; generic-hmac, GitHub, Bitbucket and Nexus use HMAC, while gitlab compares X-Gitlab-Token. -- flux-oidc (REASONED; flux, flux-oidc): generic-oidc, introduced in Flux 2.9, rejects secretRef and validates bearer tokens; constrain issuer validations and audience, whose default is notification-controller. -- flux-rbac (REASONED; flux): Only kustomize-controller and helm-controller hold cluster-admin; other controllers still have meaningful permissions including Secret access. -- flux-lockdown (REASONED; flux): Use scoped reconciliation ServiceAccounts, --no-cross-namespace-refs and --no-remote-bases; --default-service-account fills an omission, not an explicit serviceAccountName. -- verify-argo (REASONED; argo-docs, argo, curl): Anonymous application listing should fail with 401 while a valid token lists the expected application; repeat at origin and CLI/gRPC and inspect effective RBAC. -- verify-bootstrap (REASONED; argo-docs): Absence of the bootstrap Secret proves removal of that material, not password rotation. -- verify-inventory (REASONED; argo, flux): Inventory Services, routes, host ports, policies and component sockets; node-local ss and policy objects alone do not establish cluster isolation. -- verify-repo-metrics (REASONED; argo): Base policy permits HTTP 200 metrics to monitoring and unrelated pods; narrowed policy should admit only monitoring, with a successful positive control. -- verify-server-metrics (REASONED; argo): Repeat on server 8083: only authorized monitoring should receive metrics after narrowing ingress; a failed control is inconclusive. -- verify-profiler (REASONED; argo): From an allowed pod, pprof cmdline should return 401 disabled, 200 for exact true, then 401 for true plus newline; keep metrics as control. -- verify-flux (REASONED; flux, python, curl): Identical generic-hmac bodies without and with X-Signature should be rejected and accepted with reconciliation; status is type/version dependent. The cited curl curl-8_14_1 lines show --data-binary posting data exactly as specified, preserving newlines and carriage returns that --data strips from @file input, reading @- from stdin and @filename from a file, and posting like --data's POST request. -- verify-receiver-status (REASONED; flux): Read receiver name and Ready condition without exposing webhookPath or condition messages; origin checks distinguish receiver rejection from proxy rejection. - - -Argo CD and Flux reconcile a git repository into a Kubernetes cluster, so both hold -cluster-admin-equivalent power: whoever can drive the controller, or change what it reconciles, can -deploy anything it can reach, across the clusters it is registered against and to the extent the controller's Kubernetes RBAC, its registered-cluster credentials, and project restrictions allow. Three exposures follow -from that and apply to both. The blast radius is the cluster (or clusters), so administrative access -deserves the same care as the Kubernetes API itself ([kubernetes.md](kubernetes.md)). Git is an -authorization boundary: whoever can merge to a reconciled repository, or redirect a source, exercises -the controller's permissions, so platform repositories need reviewed changes and separation from -tenant repositories. And the credentials the controllers hold (git deploy keys, cluster Secrets) -should be read-only and narrowly scoped where the work allows. Handle every credential class per -[secrets.md](secrets.md): bootstrap tokens, git and registry credentials (Flux `OCIRepository`/`HelmRepository` -`.spec.secretRef`), cluster credentials, webhook secrets, and decryption keys. Never commit plaintext -credentials or base64-only Secret manifests; encrypted SOPS files and SealedSecret resources may be -committed, but Sealed Secrets is a separate controller and SOPS/age decryption keys (Flux references them -from the Kustomization) must be protected and never committed. For Argo CD, prefer destination-cluster -secret operators, because config-management plugins can surface decrypted values through Redis and the -repo-server. The tools differ in one way that shapes -the rest: Argo CD runs a network-facing API and UI with its own accounts, while Flux has no standalone -management UI or user-login API of its own, and is driven through Kubernetes custom resources under the -API server's authentication and RBAC (its controllers still expose in-cluster HTTP endpoints, and a -separately installed Flux dashboard brings its own login and exposure to harden). Control-plane exposure itself, the API server and etcd, stays in -[kubernetes.md](kubernetes.md). Egress is a surface too: both controllers fetch from Git, container and -chart registries, the Kubernetes API, identity/KMS, and notification targets, and Argo CD's repository -allowlist does not constrain remote bases or Helm chart dependencies while its webhook path has an SSRF -consideration, so restrict controller egress to the destinations each deployment needs and apply the -metadata protections in [egress-metadata.md](egress-metadata.md), keeping only the metadata access the -identity mechanism requires (for Flux tenant reconciliation, `--no-remote-bases=true` where applicable). - -## Argo CD - -The install generates an initial `admin` password and stores it in cleartext in a Secret named -`argocd-initial-admin-secret`, in the `password` field. Rotate it and then delete the Secret: -`argocd account update-password` changes the password, and the vendor says the Secret "can safely be -deleted" afterward since it only holds the bootstrap value. Deleting the Secret does not by itself -rotate the credential, so change the password first. Give `argocd account update-password` the current -and new passwords at the tool's prompt, not as arguments: `--current-password` and `--new-password` -would put them in its argv. Sources has no reference for the command prompting (its Argo CD user -management entry is described as covering disabling admin and changing the password), so confirm -that it does before relying on it. A prompt keeps the passwords out of the command text, shell -history and shell tracing; the account owner and root can still read them in the `argocd` process -while it runs. `admin` is the only built-in user and a -superuser with unrestricted access; once you have configured SSO (OIDC or Dex) and granted named -roles, disable it with `admin.enabled: "false"` in the `argocd-cm` ConfigMap, and pair Argo CD logins -with your identity provider's MFA ([mfa.md](mfa.md)). +The install generates an initial `admin` password and stores it in cleartext in a Secret named +`argocd-initial-admin-secret`, in the `password` field. Rotate it and then delete the Secret: +`argocd account update-password` changes the password, and the vendor says the Secret "can safely be +deleted" afterward since it only holds the bootstrap value. Deleting the Secret does not by itself +rotate the credential, so change the password first. Give `argocd account update-password` the current +and new passwords at the tool's prompt, not as arguments: `--current-password` and `--new-password` +would put them in its argv. Sources has no reference for the command prompting (its Argo CD user +management entry is described as covering disabling admin and changing the password), so confirm +that it does before relying on it. A prompt keeps the passwords out of the command text, shell +history and shell tracing; the account owner and root can still read them in the `argocd` process +while it runs. `admin` is the only built-in user and a +superuser with unrestricted access; once you have configured SSO (OIDC or Dex) and granted named +roles, disable it with `admin.enabled: "false"` in the `argocd-cm` ConfigMap, and pair Argo CD logins +with your identity provider's MFA ([mfa.md](mfa.md)). The quickstart does not expose the API and UI outside the cluster; `argocd-server` is a ClusterIP Service, and reaching it is a deliberate act through a LoadBalancer, an Ingress, a Gateway route, or @@ -729,3 +463,295 @@ they were not rechecked online. Remaining source limits are identified above. - Argo CD Redis password initializer command (v3.5.3; commit c9c369efcc5b2a0bd720803f8d14a1c3eaddf579; checked October 2026): https://github.com/argoproj/argo-cd/blob/v3.5.3/cmd/argocd/commands/admin/redis_initial_password.go#L41-L42 - Argo CD additive default-role enforcement (v3.5.3; commit c9c369efcc5b2a0bd720803f8d14a1c3eaddf579; checked October 2026): https://github.com/argoproj/argo-cd/blob/v3.5.3/util/rbac/rbac.go#L401-L406 - Argo CD default-role ConfigMap loading (v3.5.3; commit c9c369efcc5b2a0bd720803f8d14a1c3eaddf579; checked October 2026): https://github.com/argoproj/argo-cd/blob/v3.5.3/util/rbac/rbac.go#L540-L541 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/gitops-controllers.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- argo: Argo CD v3.5.3 source (c9c369efcc5b2a0bd720803f8d14a1c3eaddf579) +- flux-oidc: Flux OIDC introduction (v2.9) +- argo-docs: Argo CD documentation (unknown) +- flux: Flux documentation (unknown) +- dex: Dex v2.45.1 source (v2.45.1) +- redis-image: docker-library-redis v8.2.3 source (bcbc692165ce2241e194035041c5b5e01d2e1a5f) +- redis-source: Redis 8.2.3 source (8.2.3) +- curl: curl (curl-8_14_1) +- python: Python documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- git-boundary (REASONED; argo-docs, flux): Repository writers exercise reconciliation permissions; review platform changes, separate tenants and scope credentials. +- secret-storage (REASONED; argo-docs, flux): Protect bootstrap, registry, cluster, webhook and decryption credentials; commit only encrypted secrets and keep SOPS/age keys out of Git. +- egress (REASONED; argo-docs, flux): Restrict fetch, API, identity and notification egress; Argo repository allowlists do not constrain remote bases or Helm dependencies. +- argo-bootstrap (REASONED; argo-docs): Rotate the initial admin password before deleting argocd-initial-admin-secret; deletion alone does not rotate it. +- argo-sso (REASONED; argo-docs): The built-in admin is unrestricted; configure named SSO roles and IdP MFA, then set admin.enabled=false. +- argo-origin (REASONED; argo-docs, argo): Keep the ClusterIP origin private behind authenticated TLS and protect both REST/browser and CLI/gRPC routes. +- argo-server (REASONED; argo): Server defaults to 0.0.0.0:8080; the Service maps 80 and 443 to it. +- argo-tls (REASONED; argo, argo-docs): Server --insecure/server.insecure defaults false and disables backend TLS, not authentication; use only behind protected TLS termination. +- argo-client-tls (REASONED; argo-docs): argocd login --insecure skips client certificate verification; the server initially uses a self-signed certificate. +- argo-metrics (REASONED; argo): Server HTTP metrics on 8083 are unauthenticated and bind to --address, not --metrics-address. +- argo-profiler (REASONED; argo): Server and repo-server pprof paths return 401 unless the profiler file contains exactly true with no newline; enabled profiling has no caller authentication. +- argo-profiler-file (REASONED; argo): ARGOCD_ENABLE_PROFILER_FILE_PATH defaults to /home/argocd/params/profiler.enabled; the server mounts ConfigMap key server.profile.enabled there. +- repo-grpc (REASONED; argo): Repo-server gRPC binds 0.0.0.0:8081 with TLS by default; mTLS is skipped without the client CA, separate from the serving certificate. +- repo-http (REASONED; argo): Repo-server HTTP metrics and health bind 0.0.0.0:8084 without an authentication wrapper. +- repo-limits (REASONED; argo): Repo-server requires verified client certificates with a loaded client CA; missing or unstatable serving files trigger a session self-signed certificate, while an invalid present pair errors. No authorization interceptor appears; service method authorization is unverified. +- controller-metrics (REASONED; argo): Application-controller metrics and health use 8082; the command passes that port to the controller, but the fetched files do not establish the bind address. +- applicationset-webhook (REASONED; argo): ApplicationSet webhook binds :7000; its container and Service declare 7000. +- applicationset-metrics (REASONED; argo): ApplicationSet metrics bind :8080; its container and Service declare 8080. +- applicationset-health (REASONED; argo): ApplicationSet health probes bind :8081 without a container or Service port declaration; missing declarations do not close sockets. +- notifications (REASONED; argo): Notifications HTTP metrics bind 0.0.0.0:9001 without authentication; a metrics Service and TCP liveness probe exist without containerPort. +- dex-http (REASONED; argo): Dex starts only with nonempty generated configuration; HTTP defaults to TLS on 0.0.0.0:5556 using /tmp/tls.crt and /tmp/tls.key. +- dex-grpc (REASONED; argo, dex): Generated Dex gRPC uses 0.0.0.0:5557 without TLS or client-auth settings; Dex v2.45.1 source sets no transport credentials or authentication interceptor for this configuration, while API method authorization is unverified; this assumes the deployed ghcr.io/dexidp/dex:v2.45.1 tag was built from Dex v2.45.1. +- dex-telemetry (REASONED; argo): Dex telemetry uses HTTP on 0.0.0.0:5558; all three Dex ports have Services. +- redis-password (REASONED; argo, redis-image, redis-source): Redis receives --requirepass from argocd-redis/auth; secret-init generates and checks that Secret. The redis:8.2.3-alpine tag, taken as built from docker-library-redis v8.2.3, uses Redis wildcard bind defaults and patches protected mode off. +- redis-tls (REASONED; argo): Base manifests configure no Redis TLS and the Argo Redis client defaults --redis-use-tls=false; protect traffic, Secret and cache. +- component-policies (REASONED; argo): Base policies restrict repo gRPC, Redis and Dex callers by component; enforcement needs a supporting CNI. +- broad-policies (REASONED; argo): Server policy allows all ingress; other metrics and ApplicationSet webhook allowances admit every namespace. Narrow existing additive allowances and egress separately. +- argo-anonymous (REASONED; argo, argo-docs): users.anonymous.enabled defaults off; anonymous callers otherwise inherit policy.default. +- argo-disable-auth (REASONED; argo): server.disable.auth defaults false; inspect effective args and environment because anonymous-off does not compensate for disabled authentication. +- argo-default-role (REASONED; argo, argo-docs): Base RBAC ConfigMap omits policy.default; the inspected loader supplies an empty role to the enforcer, which skips its additive default-role check. Default-role grants are additive and cannot be revoked by a later subject rule; define scoped policy.csv roles and group mappings. Server-side wiring remains unverified. +- argo-webhook (REASONED; argo-docs): /api/webhook accepts unauthenticated refresh events without a shared secret; configure one to limit spoofed reconciliation and resource exhaustion. +- argo-projects (REASONED; argo-docs): Restrict AppProjects including the built-in default project; project constraints do not reduce a compromised controller's Kubernetes credentials. +- argo-cluster-rbac (REASONED; argo-docs): Reduce controller and registered-cluster argocd-manager write permissions to needed namespaces/resources while retaining required read access. +- flux-api (REASONED; flux): Flux has no standalone management login/API; Kubernetes authenticates its custom-resource operations, while separate dashboards need their own controls. +- flux-bootstrap (REASONED; flux): GitHub bootstrap --token-auth retains a PAT in flux-system; --token-auth=false creates a read-only SSH deploy key by default. Protect bootstrap input and Secrets. +- flux-writeback (REASONED; flux): Image automation needs Git write access, including --read-write-key=true for the bootstrap key; scope the repository and protect update branches and objects. +- flux-artifacts (REASONED; flux): Source-controller serves fetched artifacts over internal HTTP; enforced NetworkPolicy, not ClusterIP alone, isolates them. +- flux-receiver-port (REASONED; flux): Webhook receiver listens on 9292 behind Service port 80; external exposure needs a route and TLS termination. +- flux-receiver-types (REASONED; flux): generic validates nothing; generic-hmac, GitHub, Bitbucket and Nexus use HMAC, while gitlab compares X-Gitlab-Token. +- flux-oidc (REASONED; flux, flux-oidc): generic-oidc, introduced in Flux 2.9, rejects secretRef and validates bearer tokens; constrain issuer validations and audience, whose default is notification-controller. +- flux-rbac (REASONED; flux): Only kustomize-controller and helm-controller hold cluster-admin; other controllers still have meaningful permissions including Secret access. +- flux-lockdown (REASONED; flux): Use scoped reconciliation ServiceAccounts, --no-cross-namespace-refs and --no-remote-bases; --default-service-account fills an omission, not an explicit serviceAccountName. +- verify-argo (REASONED; argo-docs, argo, curl): Anonymous application listing should fail with 401 while a valid token lists the expected application; repeat at origin and CLI/gRPC and inspect effective RBAC. +- verify-bootstrap (REASONED; argo-docs): Absence of the bootstrap Secret proves removal of that material, not password rotation. +- verify-inventory (REASONED; argo, flux): Inventory Services, routes, host ports, policies and component sockets; node-local ss and policy objects alone do not establish cluster isolation. +- verify-repo-metrics (REASONED; argo): Base policy permits HTTP 200 metrics to monitoring and unrelated pods; narrowed policy should admit only monitoring, with a successful positive control. +- verify-server-metrics (REASONED; argo): Repeat on server 8083: only authorized monitoring should receive metrics after narrowing ingress; a failed control is inconclusive. +- verify-profiler (REASONED; argo): From an allowed pod, pprof cmdline should return 401 disabled, 200 for exact true, then 401 for true plus newline; keep metrics as control. +- verify-flux (REASONED; flux, python, curl): Identical generic-hmac bodies without and with X-Signature should be rejected and accepted with reconciliation; status is type/version dependent. The cited curl curl-8_14_1 lines show --data-binary posting data exactly as specified, preserving newlines and carriage returns that --data strips from @file input, reading @- from stdin and @filename from a file, and posting like --data's POST request. +- verify-receiver-status (REASONED; flux): Read receiver name and Ready condition without exposing webhookPath or condition messages; origin checks distinguish receiver rejection from proxy rejection. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "908b108054ff323b7d30f6ec6652728a0d065571804b124943b579b8f09f77cc", + "components": { + "argo": { + "name": "Argo CD v3.5.3 source", + "basis": "c9c369efcc5b2a0bd720803f8d14a1c3eaddf579", + "sources": { + "sddf4853ad503": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-service.yaml#L9-L20", + "s6e8c96d23e05": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-server/commands/argocd_server.go#L307-L323", + "s96065cc09776": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/server.go#L512-L535", + "s7cc690d1bf69": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/server.go#L668-L677", + "s028a4769d6c3": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/metrics/metrics.go#L74-L100", + "sc52bbd14bff4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/profile/profile.go#L11-L30", + "s98b27313f615": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-repo-server/commands/argocd_repo_server.go#L180-L212", + "sb0648e746fb9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L406-L408", + "s2f5af86c62d1": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L475-L481", + "s9bca471ac6d9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-repo-server/commands/argocd_repo_server.go#L257-L280", + "s4bbc848d9923": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/repo-server/argocd-repo-server-deployment.yaml#L367-L384", + "sf98b6bc4f5dc": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/application-controller/argocd-application-controller-statefulset.yaml#L360-L365", + "sf4dd846c82c4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/common/common.go#L75-L92", + "s49a7c83d9371": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-applicationset-controller/commands/applicationset_controller.go#L294-L296", + "s97a588d8a83f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/applicationset-controller/argocd-applicationset-controller-service.yaml#L9-L20", + "s0a85b38dfca8": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-notification/commands/argocd_notification.go#L149-L154", + "s5e141e32ed9c": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/notification/argocd-notifications-controller-metrics-service.yaml#L9-L16", + "s8efcc66f9cb4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-dex/commands/argocd_dex.go#L82-L119", + "s30c391d9dc23": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-dex/commands/argocd_dex.go#L143-L146", + "se477de2fdfa0": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/dex/config.go#L16-L152", + "s56c41c02ba8b": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-service.yaml#L9-L25", + "s996c72c5db28": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/redis/argocd-redis-deployment.yaml#L18-L58", + "s34909d7a1932": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/cache/cache.go#L233-L238", + "s1a13cc06db6d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/repo-server/argocd-repo-server-network-policy.yaml#L9-L35", + "s4bbf38e83770": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/redis/argocd-redis-network-policy.yaml#L9-L28", + "sadccbbcffcd8": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-network-policy.yaml#L9-L29", + "s743c8187cbd7": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-network-policy.yaml#L9-L16", + "s49f63c21c3cd": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/application-controller/argocd-application-controller-network-policy.yaml#L9-L19", + "sa258c4109d24": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/applicationset-controller/argocd-applicationset-controller-network-policy.yaml#L9-L22", + "sa49c30e332ed": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/notification/argocd-notifications-controller-network-policy.yaml#L9-L20", + "s68b67007da6b": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/settings/settings.go#L1631-L1638", + "s01a52daf078f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L121-L126", + "s210b24f04535": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/config/argocd-cmd-params-cm.yaml#L1-L7", + "s0f7f1b3404d9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/config/argocd-rbac-cm.yaml#L1-L7", + "s85445da68ce7": "https://github.com/argoproj/argo-cd/blob/v3.5.3/common/common.go#L89-L90", + "s357d253fc789": "https://github.com/argoproj/argo-cd/blob/v3.5.3/cmd/argocd-repo-server/commands/argocd_repo_server.go#L257-L260", + "s9df1243dde48": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-application-controller/commands/argocd_application_controller.go#L192-L209", + "s0abbc26d61e4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-application-controller/commands/argocd_application_controller.go#L272-L273", + "sfcaf2e03154d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/reposerver/server.go#L49-L85", + "s25a5da2c5e7c": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/reposerver/server.go#L94-L150", + "s73653423b85d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/tls/tls.go#L428-L506", + "s7c7bf274e3ef": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-deployment.yaml#L38-L42", + "s08939ffb906f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd/commands/admin/redis_initial_password.go#L22-L89", + "s3fcdf1601776": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L454-L467", + "se987196f20c7": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L539-L547", + "s3cb98aa5348d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L399-L425", + "s9bfcf07a6bcc": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L314-L346", + "s32323471d785": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L35-L42" + } + }, + "flux-oidc": { + "name": "Flux OIDC introduction", + "basis": "v2.9", + "sources": { + "s1ea193b2b3cc": "https://fluxcd.io/blog/2026/06/flux-v2.9.0/" + } + }, + "argo-docs": { + "name": "Argo CD documentation", + "basis": "unknown", + "sources": { + "s51e21b74ece6": "https://argo-cd.readthedocs.io/en/stable/operator-manual/security/", + "s02463a4fb268": "https://argo-cd.readthedocs.io/en/stable/operator-manual/secret-management/", + "s2efb9ee633af": "https://argo-cd.readthedocs.io/en/stable/getting_started/", + "sc2590df86f4e": "https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/", + "sf5f1c41f89bd": "https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/", + "s3767d400e364": "https://argo-cd.readthedocs.io/en/stable/operator-manual/ingress/", + "sdac90731b97d": "https://argo-cd.readthedocs.io/en/stable/operator-manual/webhook/", + "s9357e3b59e84": "https://argo-cd.readthedocs.io/en/stable/user-guide/projects/" + } + }, + "flux": { + "name": "Flux documentation", + "basis": "unknown", + "sources": { + "s493859f3b848": "https://fluxcd.io/flux/security/best-practices/", + "s4dcd744a2444": "https://fluxcd.io/flux/guides/mozilla-sops/", + "sd8618bbbc628": "https://fluxcd.io/flux/components/source/ocirepositories/#secret-reference", + "sf3f80d9941c8": "https://fluxcd.io/flux/installation/configuration/multitenancy/", + "s0669b973542c": "https://fluxcd.io/flux/security/#controller-permissions", + "s13bd7f0204b0": "https://fluxcd.io/flux/installation/bootstrap/github/", + "s8759854c1bdf": "https://fluxcd.io/flux/components/image/imageupdateautomations/", + "s0acf42beed45": "https://fluxcd.io/flux/guides/webhook-receivers/", + "s35bb9a844dcf": "https://fluxcd.io/flux/components/notification/receivers/" + } + }, + "dex": { + "name": "Dex v2.45.1 source", + "basis": "v2.45.1", + "sources": { + "s3b9512b79dea": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/serve.go#L151-L199", + "s46eb2feaa84f": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/serve.go#L553-L572", + "s55b453f266f3": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/config.go#L255-L265" + } + }, + "redis-image": { + "name": "docker-library-redis v8.2.3 source", + "basis": "bcbc692165ce2241e194035041c5b5e01d2e1a5f", + "sources": { + "s67a2f7553c9a": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/Dockerfile#L81-L87", + "s30643c91686d": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/Dockerfile#L151-L154", + "sd04db8453288": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/docker-entrypoint.sh#L93-L111" + } + }, + "redis-source": { + "name": "Redis 8.2.3 source", + "basis": "8.2.3", + "sources": { + "s667fc16bf18f": "https://github.com/redis/redis/blob/8.2.3/src/server.h#L142-L143", + "saa497d27f72f": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2199-L2221", + "s0f96763525b7": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2638-L2673", + "s9b50a7c52721": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2983-L2996" + } + }, + "curl": { + "name": "curl", + "basis": "curl-8_14_1", + "sources": { + "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md", + "s9537bb790553": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data-binary.md#L4-L32", + "sd3b039bafbf6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data.md#L4-L49" + } + }, + "python": { + "name": "Python documentation", + "basis": "unknown", + "sources": { + "s7a54556e5dac": "https://docs.python.org/3/library/hmac.html", + "s31fccd7d50e5": "https://docs.python.org/3/library/sys.html#sys.stdin" + } + } + }, + "claims": { + "git-boundary": {"text": "Repository writers exercise reconciliation permissions; review platform changes, separate tenants and scope credentials.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s51e21b74ece6", "flux:s493859f3b848"], "status": "REASONED"}, + "secret-storage": {"text": "Protect bootstrap, registry, cluster, webhook and decryption credentials; commit only encrypted secrets and keep SOPS/age keys out of Git.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s02463a4fb268", "flux:s4dcd744a2444", "flux:sd8618bbbc628"], "status": "REASONED"}, + "egress": {"text": "Restrict fetch, API, identity and notification egress; Argo repository allowlists do not constrain remote bases or Helm dependencies.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s51e21b74ece6", "flux:sf3f80d9941c8"], "status": "REASONED"}, + "argo-bootstrap": {"text": "Rotate the initial admin password before deleting argocd-initial-admin-secret; deletion alone does not rotate it.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:sc2590df86f4e"], "status": "REASONED"}, + "argo-sso": {"text": "The built-in admin is unrestricted; configure named SSO roles and IdP MFA, then set admin.enabled=false.", "components": ["argo-docs"], "sources": ["argo-docs:sc2590df86f4e", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, + "argo-origin": {"text": "Keep the ClusterIP origin private behind authenticated TLS and protect both REST/browser and CLI/gRPC routes.", "components": ["argo-docs", "argo"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:s3767d400e364", "argo:sddf4853ad503"], "status": "REASONED"}, + "argo-server": {"text": "Server defaults to 0.0.0.0:8080; the Service maps 80 and 443 to it.", "components": ["argo"], "sources": ["argo:s6e8c96d23e05", "argo:sddf4853ad503", "argo:sf4dd846c82c4"], "status": "REASONED"}, + "argo-tls": {"text": "Server --insecure/server.insecure defaults false and disables backend TLS, not authentication; use only behind protected TLS termination.", "components": ["argo", "argo-docs"], "sources": ["argo:s6e8c96d23e05", "argo-docs:s3767d400e364"], "status": "REASONED"}, + "argo-client-tls": {"text": "argocd login --insecure skips client certificate verification; the server initially uses a self-signed certificate.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:s3767d400e364"], "status": "REASONED"}, + "argo-metrics": {"text": "Server HTTP metrics on 8083 are unauthenticated and bind to --address, not --metrics-address.", "components": ["argo"], "sources": ["argo:s96065cc09776", "argo:s7cc690d1bf69", "argo:s028a4769d6c3", "argo:sf4dd846c82c4", "argo:s6e8c96d23e05"], "status": "REASONED"}, + "argo-profiler": {"text": "Server and repo-server pprof paths return 401 unless the profiler file contains exactly true with no newline; enabled profiling has no caller authentication.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:s028a4769d6c3", "argo:s98b27313f615"], "status": "REASONED"}, + "argo-profiler-file": {"text": "ARGOCD_ENABLE_PROFILER_FILE_PATH defaults to /home/argocd/params/profiler.enabled; the server mounts ConfigMap key server.profile.enabled there.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:sb0648e746fb9", "argo:s2f5af86c62d1"], "status": "REASONED"}, + "repo-grpc": {"text": "Repo-server gRPC binds 0.0.0.0:8081 with TLS by default; mTLS is skipped without the client CA, separate from the serving certificate.", "components": ["argo"], "sources": ["argo:s9bca471ac6d9", "argo:s4bbc848d9923", "argo:sf4dd846c82c4", "argo:s85445da68ce7", "argo:s357d253fc789"], "status": "REASONED"}, + "repo-http": {"text": "Repo-server HTTP metrics and health bind 0.0.0.0:8084 without an authentication wrapper.", "components": ["argo"], "sources": ["argo:s98b27313f615", "argo:s9bca471ac6d9", "argo:sf4dd846c82c4", "argo:s85445da68ce7", "argo:s357d253fc789"], "status": "REASONED"}, + "repo-limits": {"text": "Repo-server requires verified client certificates with a loaded client CA; missing or unstatable serving files trigger a session self-signed certificate, while an invalid present pair errors. No authorization interceptor appears; service method authorization is unverified.", "components": ["argo"], "sources": ["argo:sfcaf2e03154d", "argo:s25a5da2c5e7c", "argo:s73653423b85d"], "status": "REASONED"}, + "controller-metrics": {"text": "Application-controller metrics and health use 8082; the command passes that port to the controller, but the fetched files do not establish the bind address.", "components": ["argo"], "sources": ["argo:sf98b6bc4f5dc", "argo:s9df1243dde48", "argo:s0abbc26d61e4", "argo:sf4dd846c82c4"], "status": "REASONED"}, + "applicationset-webhook": {"text": "ApplicationSet webhook binds :7000; its container and Service declare 7000.", "components": ["argo"], "sources": ["argo:s49a7c83d9371", "argo:s97a588d8a83f"], "status": "REASONED"}, + "applicationset-metrics": {"text": "ApplicationSet metrics bind :8080; its container and Service declare 8080.", "components": ["argo"], "sources": ["argo:s49a7c83d9371", "argo:s97a588d8a83f"], "status": "REASONED"}, + "applicationset-health": {"text": "ApplicationSet health probes bind :8081 without a container or Service port declaration; missing declarations do not close sockets.", "components": ["argo"], "sources": ["argo:s49a7c83d9371"], "status": "REASONED"}, + "notifications": {"text": "Notifications HTTP metrics bind 0.0.0.0:9001 without authentication; a metrics Service and TCP liveness probe exist without containerPort.", "components": ["argo"], "sources": ["argo:s0a85b38dfca8", "argo:s5e141e32ed9c"], "status": "REASONED"}, + "dex-http": {"text": "Dex starts only with nonempty generated configuration; HTTP defaults to TLS on 0.0.0.0:5556 using /tmp/tls.crt and /tmp/tls.key.", "components": ["argo"], "sources": ["argo:s8efcc66f9cb4", "argo:s30c391d9dc23", "argo:se477de2fdfa0"], "status": "REASONED"}, + "dex-grpc": {"text": "Generated Dex gRPC uses 0.0.0.0:5557 without TLS or client-auth settings; Dex v2.45.1 source sets no transport credentials or authentication interceptor for this configuration, while API method authorization is unverified; this assumes the deployed ghcr.io/dexidp/dex:v2.45.1 tag was built from Dex v2.45.1.", "components": ["argo", "dex"], "sources": ["argo:se477de2fdfa0", "argo:s56c41c02ba8b", "argo:s7c7bf274e3ef", "dex:s3b9512b79dea", "dex:s46eb2feaa84f", "dex:s55b453f266f3"], "status": "REASONED"}, + "dex-telemetry": {"text": "Dex telemetry uses HTTP on 0.0.0.0:5558; all three Dex ports have Services.", "components": ["argo"], "sources": ["argo:se477de2fdfa0", "argo:s56c41c02ba8b"], "status": "REASONED"}, + "redis-password": {"text": "Redis receives --requirepass from argocd-redis/auth; secret-init generates and checks that Secret. The redis:8.2.3-alpine tag, taken as built from docker-library-redis v8.2.3, uses Redis wildcard bind defaults and patches protected mode off.", "components": ["argo", "redis-image", "redis-source"], "sources": ["argo:s996c72c5db28", "argo:s08939ffb906f", "redis-image:s67a2f7553c9a", "redis-image:s30643c91686d", "redis-image:sd04db8453288", "redis-source:s667fc16bf18f", "redis-source:saa497d27f72f", "redis-source:s0f96763525b7", "redis-source:s9b50a7c52721"], "status": "REASONED"}, + "redis-tls": {"text": "Base manifests configure no Redis TLS and the Argo Redis client defaults --redis-use-tls=false; protect traffic, Secret and cache.", "components": ["argo"], "sources": ["argo:s996c72c5db28", "argo:s34909d7a1932"], "status": "REASONED"}, + "component-policies": {"text": "Base policies restrict repo gRPC, Redis and Dex callers by component; enforcement needs a supporting CNI.", "components": ["argo"], "sources": ["argo:s1a13cc06db6d", "argo:s4bbf38e83770", "argo:sadccbbcffcd8"], "status": "REASONED"}, + "broad-policies": {"text": "Server policy allows all ingress; other metrics and ApplicationSet webhook allowances admit every namespace. Narrow existing additive allowances and egress separately.", "components": ["argo"], "sources": ["argo:s743c8187cbd7", "argo:s49f63c21c3cd", "argo:sa258c4109d24", "argo:sa49c30e332ed"], "status": "REASONED"}, + "argo-anonymous": {"text": "users.anonymous.enabled defaults off; anonymous callers otherwise inherit policy.default.", "components": ["argo", "argo-docs"], "sources": ["argo:s68b67007da6b", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, + "argo-disable-auth": {"text": "server.disable.auth defaults false; inspect effective args and environment because anonymous-off does not compensate for disabled authentication.", "components": ["argo"], "sources": ["argo:s6e8c96d23e05", "argo:s01a52daf078f", "argo:s210b24f04535"], "status": "REASONED"}, + "argo-default-role": {"text": "Base RBAC ConfigMap omits policy.default; the inspected loader supplies an empty role to the enforcer, which skips its additive default-role check. Default-role grants are additive and cannot be revoked by a later subject rule; define scoped policy.csv roles and group mappings. Server-side wiring remains unverified.", "components": ["argo", "argo-docs"], "sources": ["argo:s0f7f1b3404d9", "argo:s3fcdf1601776", "argo:se987196f20c7", "argo:s3cb98aa5348d", "argo:s9bfcf07a6bcc", "argo:s32323471d785", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, + "argo-webhook": {"text": "/api/webhook accepts unauthenticated refresh events without a shared secret; configure one to limit spoofed reconciliation and resource exhaustion.", "components": ["argo-docs"], "sources": ["argo-docs:sdac90731b97d"], "status": "REASONED"}, + "argo-projects": {"text": "Restrict AppProjects including the built-in default project; project constraints do not reduce a compromised controller's Kubernetes credentials.", "components": ["argo-docs"], "sources": ["argo-docs:s9357e3b59e84", "argo-docs:s51e21b74ece6"], "status": "REASONED"}, + "argo-cluster-rbac": {"text": "Reduce controller and registered-cluster argocd-manager write permissions to needed namespaces/resources while retaining required read access.", "components": ["argo-docs"], "sources": ["argo-docs:s51e21b74ece6"], "status": "REASONED"}, + "flux-api": {"text": "Flux has no standalone management login/API; Kubernetes authenticates its custom-resource operations, while separate dashboards need their own controls.", "components": ["flux"], "sources": ["flux:s493859f3b848", "flux:s0669b973542c"], "status": "REASONED"}, + "flux-bootstrap": {"text": "GitHub bootstrap --token-auth retains a PAT in flux-system; --token-auth=false creates a read-only SSH deploy key by default. Protect bootstrap input and Secrets.", "components": ["flux"], "sources": ["flux:s13bd7f0204b0"], "status": "REASONED"}, + "flux-writeback": {"text": "Image automation needs Git write access, including --read-write-key=true for the bootstrap key; scope the repository and protect update branches and objects.", "components": ["flux"], "sources": ["flux:s13bd7f0204b0", "flux:s8759854c1bdf"], "status": "REASONED"}, + "flux-artifacts": {"text": "Source-controller serves fetched artifacts over internal HTTP; enforced NetworkPolicy, not ClusterIP alone, isolates them.", "components": ["flux"], "sources": ["flux:s493859f3b848"], "status": "REASONED"}, + "flux-receiver-port": {"text": "Webhook receiver listens on 9292 behind Service port 80; external exposure needs a route and TLS termination.", "components": ["flux"], "sources": ["flux:s0acf42beed45"], "status": "REASONED"}, + "flux-receiver-types": {"text": "generic validates nothing; generic-hmac, GitHub, Bitbucket and Nexus use HMAC, while gitlab compares X-Gitlab-Token.", "components": ["flux"], "sources": ["flux:s35bb9a844dcf"], "status": "REASONED"}, + "flux-oidc": {"text": "generic-oidc, introduced in Flux 2.9, rejects secretRef and validates bearer tokens; constrain issuer validations and audience, whose default is notification-controller.", "components": ["flux", "flux-oidc"], "sources": ["flux:s35bb9a844dcf", "flux-oidc:s1ea193b2b3cc"], "status": "REASONED"}, + "flux-rbac": {"text": "Only kustomize-controller and helm-controller hold cluster-admin; other controllers still have meaningful permissions including Secret access.", "components": ["flux"], "sources": ["flux:s0669b973542c"], "status": "REASONED"}, + "flux-lockdown": {"text": "Use scoped reconciliation ServiceAccounts, --no-cross-namespace-refs and --no-remote-bases; --default-service-account fills an omission, not an explicit serviceAccountName.", "components": ["flux"], "sources": ["flux:sf3f80d9941c8"], "status": "REASONED"}, + "verify-argo": {"text": "Anonymous application listing should fail with 401 while a valid token lists the expected application; repeat at origin and CLI/gRPC and inspect effective RBAC.", "components": ["argo-docs", "argo", "curl"], "sources": ["argo-docs:sf5f1c41f89bd", "argo:s6e8c96d23e05", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, + "verify-bootstrap": {"text": "Absence of the bootstrap Secret proves removal of that material, not password rotation.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af"], "status": "REASONED", "verify": [1]}, + "verify-inventory": {"text": "Inventory Services, routes, host ports, policies and component sockets; node-local ss and policy objects alone do not establish cluster isolation.", "components": ["argo", "flux"], "sources": ["argo:s743c8187cbd7", "flux:s493859f3b848"], "status": "REASONED", "verify": [1]}, + "verify-repo-metrics": {"text": "Base policy permits HTTP 200 metrics to monitoring and unrelated pods; narrowed policy should admit only monitoring, with a successful positive control.", "components": ["argo"], "sources": ["argo:s98b27313f615", "argo:s1a13cc06db6d"], "status": "REASONED", "verify": [2]}, + "verify-server-metrics": {"text": "Repeat on server 8083: only authorized monitoring should receive metrics after narrowing ingress; a failed control is inconclusive.", "components": ["argo"], "sources": ["argo:s028a4769d6c3", "argo:s743c8187cbd7"], "status": "REASONED"}, + "verify-profiler": {"text": "From an allowed pod, pprof cmdline should return 401 disabled, 200 for exact true, then 401 for true plus newline; keep metrics as control.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:s028a4769d6c3"], "status": "REASONED"}, + "verify-flux": {"text": "Identical generic-hmac bodies without and with X-Signature should be rejected and accepted with reconciliation; status is type/version dependent. The cited curl curl-8_14_1 lines show --data-binary posting data exactly as specified, preserving newlines and carriage returns that --data strips from @file input, reading @- from stdin and @filename from a file, and posting like --data's POST request.", "components": ["flux", "python", "curl"], "sources": ["flux:s35bb9a844dcf", "python:s7a54556e5dac", "python:s31fccd7d50e5", "curl:s3db522258e44", "curl:s9537bb790553", "curl:sd3b039bafbf6"], "status": "REASONED", "verify": [3]}, + "verify-receiver-status": {"text": "Read receiver name and Ready condition without exposing webhookPath or condition messages; origin checks distinguish receiver rejection from proxy rejection.", "components": ["flux"], "sources": ["flux:s35bb9a844dcf"], "status": "REASONED"} + } +} +```` + diff --git a/haproxy.md b/haproxy.md index 396dc915d..91505745d 100644 --- a/haproxy.md +++ b/haproxy.md @@ -1,142 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-05", - "documentation_checked": "2026-09", - "body_sha256": "afa78827cbab9d1fd7020200e3a50b28da524fc31cba3550243a014cbe4363a9", - "components": { - "haproxy": { - "name": "HAProxy", - "basis": "3.0", - "sources": { - "s5792e6ff4990": "https://docs.haproxy.org/3.0/configuration.html", - "s9c32621d6493": "https://docs.haproxy.org/3.0/management.html" - } - }, - "hsts": { - "name": "HAProxy http-after-response minimum", - "basis": "2.2+", - "sources": { - "s5792e6ff4990": "https://docs.haproxy.org/3.0/configuration.html" - } - }, - "docs": { - "name": "Supporting documentation", - "basis": "unknown", - "sources": { - "s2be90eb936bd": "https://www.haproxy.org/", - "sae992a8ac3a7": "https://www.haproxy.com/documentation/haproxy-configuration-tutorials/alerts-and-monitoring/statistics/", - "s192d42dd9feb": "https://www.authelia.com/integration/proxies/haproxy/", - "s529b0eabe2ed": "https://ssl-config.mozilla.org/" - } - }, - "openssl-cli": { - "name": "OpenSSL password command", - "basis": "3.0", - "sources": { - "s3b2ccc45875a": "https://docs.openssl.org/3.0/man1/openssl-passwd/" - } - }, - "certbot-source": { - "name": "Certbot deploy hook documentation", - "basis": "v2.3.0", - "sources": { - "sd39028a759a7": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L675" - } - }, - "cloudflare-source": { - "name": "Cloudflare Access MFA documentation", - "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", - "sources": { - "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" - } - } - }, - "claims": { - "scope": {"text": "Examples target a maintained HAProxy 3.0 installation and treat frontend, stats, runtime API and direct backend as separate surfaces.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493", "docs:s2be90eb936bd"], "status": "REASONED"}, - "pem": {"text": "Use matching chain/key in a combined PEM, built via a root-only temporary file and rename; separate key loading is also supported.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "renewal": {"text": "Rebuild the combined PEM and reload HAProxy after successful renewal using Certbot v2.3.0 --deploy-hook or an executable in /etc/letsencrypt/renewal-hooks/deploy (if the configuration directory is /etc/letsencrypt; certbot renew only).", "components": ["haproxy", "certbot-source"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493", "certbot-source:sd39028a759a7"], "status": "REASONED"}, - "tls": {"text": "Frontend :443 loads the PEM with ssl and ssl-min-ver TLSv1.2; :80 redirects to HTTPS with 301.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "ciphers": {"text": "ssl-default-bind-ciphers governs TLS 1.2 and -ciphersuites TLS 1.3; Mozilla is policy guidance, not HAProxy syntax authority.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:s529b0eabe2ed"], "status": "REASONED"}, - "privileges": {"text": "Preserve service hardening; root-started workers drop to haproxy user/group and chroot into a provisioned root-owned directory.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "timeouts": {"text": "Set connect 5s, client/server 30s and http-request 10s timeouts.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "hsts": {"text": "http-after-response (HAProxy 2.2+) covers generated 401/413 responses that http-response misses; includeSubDomains requires HTTPS on every affected subdomain.", "components": ["haproxy", "hsts"], "sources": ["haproxy:s5792e6ff4990", "hsts:s5792e6ff4990"], "status": "REASONED"}, - "backend": {"text": "Example backend is plaintext 127.0.0.1:3000 with health checks; keep it private.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "remote-backend": {"text": "Remote TLS backends require certificate verification, a trusted CA and verifyhost matching the backend name.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "basic": {"text": "Use a crypt(3)-hashed userlist and http_auth condition; $6$ works on glibc Linux, while insecure-password stores plaintext.", "components": ["haproxy", "openssl-cli"], "sources": ["haproxy:s5792e6ff4990", "openssl-cli:s3b2ccc45875a"], "status": "REASONED"}, - "mtls": {"text": "For machine access, bind verify required with ca-file requires trusted client certificates.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "mfa": {"text": "Basic is single-factor; Authelia uses its HAProxy Lua integration, or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization).", "components": ["docs", "cloudflare-source"], "sources": ["docs:s192d42dd9feb", "cloudflare-source:sdcac732541da"], "status": "REASONED"}, - "maxconn": {"text": "Global maxconn 4096 caps per-process concurrent connections; frontend maxconn 2000 caps that frontend.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "body-guard": {"text": "No single request-body-size directive: req.body_size reads advertised Content-Length; deny_status 413 rejects values above 10485760 without buffering.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "body-buffering": {"text": "Do not add http-buffer-request for this guard: it waits for a complete body or full request buffer.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "chunked": {"text": "Chunked req.body_size reflects available data, so this unbuffered guard does not meaningfully bound chunked uploads; enforce the real limit in the app.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "stats-default": {"text": "Stats is disabled until configured and enabling it adds no authentication; inventory every loaded config.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, - "stats-admin": {"text": "stats admin permits backend enable/disable/drain; omit it for monitoring and keep any admin page private.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, - "stats-auth": {"text": "Bind the example stats listener to TLS 127.0.0.1:8404 at /stats and use hashed-userlist auth instead of cleartext stats auth.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, - "runtime-default": {"text": "Runtime API is not enabled by default; default command level is operator, and level admin grants authority rather than authentication.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED"}, - "runtime-isolation": {"text": "Keep /run/haproxy/admin.sock a root-owned mode-600 Unix socket; HTTP stats auth does not protect an exposed runtime TCP socket.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED"}, - "verify-config": {"text": "haproxy -c precedes reload; the guide records shell checks but no HAProxy runtime or exposed/fixed response demonstration.", "components": ["haproxy"], "sources": ["haproxy:s9c32621d6493"], "status": "REASONED", "verify": [1]}, - "verify-redirect": {"text": "HTTP must return 301 with an HTTPS Location; transport/TLS/DNS errors are inconclusive.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, - "verify-auth": {"text": "On one protected resource, absent and wrong credentials yield 401 while valid credentials reach the expected app response; credentials travel via stdin.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, - "verify-size": {"text": "With valid credentials, 1M succeeds and 11M returns 413; remove only the deny rule in isolation to attribute refusal. These Content-Length probes do not test chunked bodies.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, - "verify-backend": {"text": "Inventory all listeners: backend 3000 must be loopback-only even if frontend checks pass, and the runtime endpoint must not be TCP.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED", "verify": [1]}, - "verify-stats": {"text": "Probe the 8404 stats listener itself via loopback with the certificate hostname: no credentials refused, valid credentials return statistics.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED", "verify": [2]}, - "verify-mtls": {"text": "With verify required, no/untrusted client certificates must fail TLS and trusted ones pass; readable cert/key controls avoid local errors, and Basic may still return 401.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [2]}, - "verify-runtime": {"text": "Check root/root mode 600 and root show info success versus nobody permission denial; reported admin command level is not proof of access control.", "components": ["haproxy"], "sources": ["haproxy:s9c32621d6493", "haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [2]} - } -} ---- # HAProxy: TLS termination and authentication - -**Version basis** +Written against HAProxy 3.0, HAProxy http-after-response minimum 2.2+, OpenSSL password command 3.0, and Certbot deploy hook documentation v2.3.0; 1 further source is pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-10-05; documentation checked September 2026. The guide records 30 claims, all reasoned from cited sources. -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-05; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- haproxy: HAProxy (3.0) -- hsts: HAProxy http-after-response minimum (2.2+) -- docs: Supporting documentation (unknown) -- openssl-cli: OpenSSL password command (3.0) -- certbot-source: Certbot deploy hook documentation (v2.3.0) -- cloudflare-source: Cloudflare Access MFA documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) - -Claims (full source mapping in the front matter): - -- scope (REASONED; haproxy, docs): Examples target a maintained HAProxy 3.0 installation and treat frontend, stats, runtime API and direct backend as separate surfaces. -- pem (REASONED; haproxy): Use matching chain/key in a combined PEM, built via a root-only temporary file and rename; separate key loading is also supported. -- renewal (REASONED; haproxy, certbot-source): Rebuild the combined PEM and reload HAProxy after successful renewal using Certbot v2.3.0 --deploy-hook or an executable in /etc/letsencrypt/renewal-hooks/deploy (if the configuration directory is /etc/letsencrypt; certbot renew only). -- tls (REASONED; haproxy): Frontend :443 loads the PEM with ssl and ssl-min-ver TLSv1.2; :80 redirects to HTTPS with 301. -- ciphers (REASONED; haproxy, docs): ssl-default-bind-ciphers governs TLS 1.2 and -ciphersuites TLS 1.3; Mozilla is policy guidance, not HAProxy syntax authority. -- privileges (REASONED; haproxy): Preserve service hardening; root-started workers drop to haproxy user/group and chroot into a provisioned root-owned directory. -- timeouts (REASONED; haproxy): Set connect 5s, client/server 30s and http-request 10s timeouts. -- hsts (REASONED; haproxy, hsts): http-after-response (HAProxy 2.2+) covers generated 401/413 responses that http-response misses; includeSubDomains requires HTTPS on every affected subdomain. -- backend (REASONED; haproxy): Example backend is plaintext 127.0.0.1:3000 with health checks; keep it private. -- remote-backend (REASONED; haproxy): Remote TLS backends require certificate verification, a trusted CA and verifyhost matching the backend name. -- basic (REASONED; haproxy, openssl-cli): Use a crypt(3)-hashed userlist and http_auth condition; $6$ works on glibc Linux, while insecure-password stores plaintext. -- mtls (REASONED; haproxy): For machine access, bind verify required with ca-file requires trusted client certificates. -- mfa (REASONED; docs, cloudflare-source): Basic is single-factor; Authelia uses its HAProxy Lua integration, or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization). -- maxconn (REASONED; haproxy): Global maxconn 4096 caps per-process concurrent connections; frontend maxconn 2000 caps that frontend. -- body-guard (REASONED; haproxy): No single request-body-size directive: req.body_size reads advertised Content-Length; deny_status 413 rejects values above 10485760 without buffering. -- body-buffering (REASONED; haproxy): Do not add http-buffer-request for this guard: it waits for a complete body or full request buffer. -- chunked (REASONED; haproxy): Chunked req.body_size reflects available data, so this unbuffered guard does not meaningfully bound chunked uploads; enforce the real limit in the app. -- stats-default (REASONED; haproxy, docs): Stats is disabled until configured and enabling it adds no authentication; inventory every loaded config. -- stats-admin (REASONED; haproxy, docs): stats admin permits backend enable/disable/drain; omit it for monitoring and keep any admin page private. -- stats-auth (REASONED; haproxy, docs): Bind the example stats listener to TLS 127.0.0.1:8404 at /stats and use hashed-userlist auth instead of cleartext stats auth. -- runtime-default (REASONED; haproxy): Runtime API is not enabled by default; default command level is operator, and level admin grants authority rather than authentication. -- runtime-isolation (REASONED; haproxy): Keep /run/haproxy/admin.sock a root-owned mode-600 Unix socket; HTTP stats auth does not protect an exposed runtime TCP socket. -- verify-config (REASONED; haproxy): haproxy -c precedes reload; the guide records shell checks but no HAProxy runtime or exposed/fixed response demonstration. -- verify-redirect (REASONED; haproxy): HTTP must return 301 with an HTTPS Location; transport/TLS/DNS errors are inconclusive. -- verify-auth (REASONED; haproxy): On one protected resource, absent and wrong credentials yield 401 while valid credentials reach the expected app response; credentials travel via stdin. -- verify-size (REASONED; haproxy): With valid credentials, 1M succeeds and 11M returns 413; remove only the deny rule in isolation to attribute refusal. These Content-Length probes do not test chunked bodies. -- verify-backend (REASONED; haproxy): Inventory all listeners: backend 3000 must be loopback-only even if frontend checks pass, and the runtime endpoint must not be TCP. -- verify-stats (REASONED; haproxy, docs): Probe the 8404 stats listener itself via loopback with the certificate hostname: no credentials refused, valid credentials return statistics. -- verify-mtls (REASONED; haproxy): With verify required, no/untrusted client certificates must fail TLS and trusted ones pass; readable cert/key controls avoid local errors, and Basic may still return 401. -- verify-runtime (REASONED; haproxy): Check root/root mode 600 and root show info success versus nobody permission denial; reported admin command level is not proof of access control. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. HAProxy's public frontend, its stats interface, its runtime API, and any directly reachable backend are separate exposure surfaces; protect and verify each one you enable. These examples target a maintained HAProxy 3.0 installation. @@ -374,3 +240,163 @@ sudo -u nobody socat - /run/haproxy/admin.sock < /dev/null # unauthorize - OpenSSL 3.0 `passwd -6` SHA-512 crypt and terminal prompting (checked October 2026): https://docs.openssl.org/3.0/man1/openssl-passwd/ - Certbot v2.3.0 deploy hooks after successful renewal (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L675 - Cloudflare Access MFA documentation at c896795b8e8bab68d05ae78d64c9cbcda1fdb104, application or policy MFA (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/haproxy.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- haproxy: HAProxy (3.0) +- hsts: HAProxy http-after-response minimum (2.2+) +- docs: Supporting documentation (unknown) +- openssl-cli: OpenSSL password command (3.0) +- certbot-source: Certbot deploy hook documentation (v2.3.0) +- cloudflare-source: Cloudflare Access MFA documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) + +Claims (full source mapping in the JSON block below): + +- scope (REASONED; haproxy, docs): Examples target a maintained HAProxy 3.0 installation and treat frontend, stats, runtime API and direct backend as separate surfaces. +- pem (REASONED; haproxy): Use matching chain/key in a combined PEM, built via a root-only temporary file and rename; separate key loading is also supported. +- renewal (REASONED; haproxy, certbot-source): Rebuild the combined PEM and reload HAProxy after successful renewal using Certbot v2.3.0 --deploy-hook or an executable in /etc/letsencrypt/renewal-hooks/deploy (if the configuration directory is /etc/letsencrypt; certbot renew only). +- tls (REASONED; haproxy): Frontend :443 loads the PEM with ssl and ssl-min-ver TLSv1.2; :80 redirects to HTTPS with 301. +- ciphers (REASONED; haproxy, docs): ssl-default-bind-ciphers governs TLS 1.2 and -ciphersuites TLS 1.3; Mozilla is policy guidance, not HAProxy syntax authority. +- privileges (REASONED; haproxy): Preserve service hardening; root-started workers drop to haproxy user/group and chroot into a provisioned root-owned directory. +- timeouts (REASONED; haproxy): Set connect 5s, client/server 30s and http-request 10s timeouts. +- hsts (REASONED; haproxy, hsts): http-after-response (HAProxy 2.2+) covers generated 401/413 responses that http-response misses; includeSubDomains requires HTTPS on every affected subdomain. +- backend (REASONED; haproxy): Example backend is plaintext 127.0.0.1:3000 with health checks; keep it private. +- remote-backend (REASONED; haproxy): Remote TLS backends require certificate verification, a trusted CA and verifyhost matching the backend name. +- basic (REASONED; haproxy, openssl-cli): Use a crypt(3)-hashed userlist and http_auth condition; $6$ works on glibc Linux, while insecure-password stores plaintext. +- mtls (REASONED; haproxy): For machine access, bind verify required with ca-file requires trusted client certificates. +- mfa (REASONED; docs, cloudflare-source): Basic is single-factor; Authelia uses its HAProxy Lua integration, or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization). +- maxconn (REASONED; haproxy): Global maxconn 4096 caps per-process concurrent connections; frontend maxconn 2000 caps that frontend. +- body-guard (REASONED; haproxy): No single request-body-size directive: req.body_size reads advertised Content-Length; deny_status 413 rejects values above 10485760 without buffering. +- body-buffering (REASONED; haproxy): Do not add http-buffer-request for this guard: it waits for a complete body or full request buffer. +- chunked (REASONED; haproxy): Chunked req.body_size reflects available data, so this unbuffered guard does not meaningfully bound chunked uploads; enforce the real limit in the app. +- stats-default (REASONED; haproxy, docs): Stats is disabled until configured and enabling it adds no authentication; inventory every loaded config. +- stats-admin (REASONED; haproxy, docs): stats admin permits backend enable/disable/drain; omit it for monitoring and keep any admin page private. +- stats-auth (REASONED; haproxy, docs): Bind the example stats listener to TLS 127.0.0.1:8404 at /stats and use hashed-userlist auth instead of cleartext stats auth. +- runtime-default (REASONED; haproxy): Runtime API is not enabled by default; default command level is operator, and level admin grants authority rather than authentication. +- runtime-isolation (REASONED; haproxy): Keep /run/haproxy/admin.sock a root-owned mode-600 Unix socket; HTTP stats auth does not protect an exposed runtime TCP socket. +- verify-config (REASONED; haproxy): haproxy -c precedes reload; the guide records shell checks but no HAProxy runtime or exposed/fixed response demonstration. +- verify-redirect (REASONED; haproxy): HTTP must return 301 with an HTTPS Location; transport/TLS/DNS errors are inconclusive. +- verify-auth (REASONED; haproxy): On one protected resource, absent and wrong credentials yield 401 while valid credentials reach the expected app response; credentials travel via stdin. +- verify-size (REASONED; haproxy): With valid credentials, 1M succeeds and 11M returns 413; remove only the deny rule in isolation to attribute refusal. These Content-Length probes do not test chunked bodies. +- verify-backend (REASONED; haproxy): Inventory all listeners: backend 3000 must be loopback-only even if frontend checks pass, and the runtime endpoint must not be TCP. +- verify-stats (REASONED; haproxy, docs): Probe the 8404 stats listener itself via loopback with the certificate hostname: no credentials refused, valid credentials return statistics. +- verify-mtls (REASONED; haproxy): With verify required, no/untrusted client certificates must fail TLS and trusted ones pass; readable cert/key controls avoid local errors, and Basic may still return 401. +- verify-runtime (REASONED; haproxy): Check root/root mode 600 and root show info success versus nobody permission denial; reported admin command level is not proof of access control. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "afa78827cbab9d1fd7020200e3a50b28da524fc31cba3550243a014cbe4363a9", + "components": { + "haproxy": { + "name": "HAProxy", + "basis": "3.0", + "sources": { + "s5792e6ff4990": "https://docs.haproxy.org/3.0/configuration.html", + "s9c32621d6493": "https://docs.haproxy.org/3.0/management.html" + } + }, + "hsts": { + "name": "HAProxy http-after-response minimum", + "basis": "2.2+", + "sources": { + "s5792e6ff4990": "https://docs.haproxy.org/3.0/configuration.html" + } + }, + "docs": { + "name": "Supporting documentation", + "basis": "unknown", + "sources": { + "s2be90eb936bd": "https://www.haproxy.org/", + "sae992a8ac3a7": "https://www.haproxy.com/documentation/haproxy-configuration-tutorials/alerts-and-monitoring/statistics/", + "s192d42dd9feb": "https://www.authelia.com/integration/proxies/haproxy/", + "s529b0eabe2ed": "https://ssl-config.mozilla.org/" + } + }, + "openssl-cli": { + "name": "OpenSSL password command", + "basis": "3.0", + "sources": { + "s3b2ccc45875a": "https://docs.openssl.org/3.0/man1/openssl-passwd/" + } + }, + "certbot-source": { + "name": "Certbot deploy hook documentation", + "basis": "v2.3.0", + "sources": { + "sd39028a759a7": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L675" + } + }, + "cloudflare-source": { + "name": "Cloudflare Access MFA documentation", + "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" + } + } + }, + "claims": { + "scope": {"text": "Examples target a maintained HAProxy 3.0 installation and treat frontend, stats, runtime API and direct backend as separate surfaces.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493", "docs:s2be90eb936bd"], "status": "REASONED"}, + "pem": {"text": "Use matching chain/key in a combined PEM, built via a root-only temporary file and rename; separate key loading is also supported.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "renewal": {"text": "Rebuild the combined PEM and reload HAProxy after successful renewal using Certbot v2.3.0 --deploy-hook or an executable in /etc/letsencrypt/renewal-hooks/deploy (if the configuration directory is /etc/letsencrypt; certbot renew only).", "components": ["haproxy", "certbot-source"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493", "certbot-source:sd39028a759a7"], "status": "REASONED"}, + "tls": {"text": "Frontend :443 loads the PEM with ssl and ssl-min-ver TLSv1.2; :80 redirects to HTTPS with 301.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "ciphers": {"text": "ssl-default-bind-ciphers governs TLS 1.2 and -ciphersuites TLS 1.3; Mozilla is policy guidance, not HAProxy syntax authority.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:s529b0eabe2ed"], "status": "REASONED"}, + "privileges": {"text": "Preserve service hardening; root-started workers drop to haproxy user/group and chroot into a provisioned root-owned directory.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "timeouts": {"text": "Set connect 5s, client/server 30s and http-request 10s timeouts.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "hsts": {"text": "http-after-response (HAProxy 2.2+) covers generated 401/413 responses that http-response misses; includeSubDomains requires HTTPS on every affected subdomain.", "components": ["haproxy", "hsts"], "sources": ["haproxy:s5792e6ff4990", "hsts:s5792e6ff4990"], "status": "REASONED"}, + "backend": {"text": "Example backend is plaintext 127.0.0.1:3000 with health checks; keep it private.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "remote-backend": {"text": "Remote TLS backends require certificate verification, a trusted CA and verifyhost matching the backend name.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "basic": {"text": "Use a crypt(3)-hashed userlist and http_auth condition; $6$ works on glibc Linux, while insecure-password stores plaintext.", "components": ["haproxy", "openssl-cli"], "sources": ["haproxy:s5792e6ff4990", "openssl-cli:s3b2ccc45875a"], "status": "REASONED"}, + "mtls": {"text": "For machine access, bind verify required with ca-file requires trusted client certificates.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "mfa": {"text": "Basic is single-factor; Authelia uses its HAProxy Lua integration, or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization).", "components": ["docs", "cloudflare-source"], "sources": ["docs:s192d42dd9feb", "cloudflare-source:sdcac732541da"], "status": "REASONED"}, + "maxconn": {"text": "Global maxconn 4096 caps per-process concurrent connections; frontend maxconn 2000 caps that frontend.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "body-guard": {"text": "No single request-body-size directive: req.body_size reads advertised Content-Length; deny_status 413 rejects values above 10485760 without buffering.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "body-buffering": {"text": "Do not add http-buffer-request for this guard: it waits for a complete body or full request buffer.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "chunked": {"text": "Chunked req.body_size reflects available data, so this unbuffered guard does not meaningfully bound chunked uploads; enforce the real limit in the app.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "stats-default": {"text": "Stats is disabled until configured and enabling it adds no authentication; inventory every loaded config.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, + "stats-admin": {"text": "stats admin permits backend enable/disable/drain; omit it for monitoring and keep any admin page private.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, + "stats-auth": {"text": "Bind the example stats listener to TLS 127.0.0.1:8404 at /stats and use hashed-userlist auth instead of cleartext stats auth.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, + "runtime-default": {"text": "Runtime API is not enabled by default; default command level is operator, and level admin grants authority rather than authentication.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED"}, + "runtime-isolation": {"text": "Keep /run/haproxy/admin.sock a root-owned mode-600 Unix socket; HTTP stats auth does not protect an exposed runtime TCP socket.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED"}, + "verify-config": {"text": "haproxy -c precedes reload; the guide records shell checks but no HAProxy runtime or exposed/fixed response demonstration.", "components": ["haproxy"], "sources": ["haproxy:s9c32621d6493"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP must return 301 with an HTTPS Location; transport/TLS/DNS errors are inconclusive.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "On one protected resource, absent and wrong credentials yield 401 while valid credentials reach the expected app response; credentials travel via stdin.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, + "verify-size": {"text": "With valid credentials, 1M succeeds and 11M returns 413; remove only the deny rule in isolation to attribute refusal. These Content-Length probes do not test chunked bodies.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, + "verify-backend": {"text": "Inventory all listeners: backend 3000 must be loopback-only even if frontend checks pass, and the runtime endpoint must not be TCP.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED", "verify": [1]}, + "verify-stats": {"text": "Probe the 8404 stats listener itself via loopback with the certificate hostname: no credentials refused, valid credentials return statistics.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED", "verify": [2]}, + "verify-mtls": {"text": "With verify required, no/untrusted client certificates must fail TLS and trusted ones pass; readable cert/key controls avoid local errors, and Basic may still return 401.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [2]}, + "verify-runtime": {"text": "Check root/root mode 600 and root show info success versus nobody permission denial; reported admin command level is not proof of access control.", "components": ["haproxy"], "sources": ["haproxy:s9c32621d6493", "haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [2]} + } +} +```` + diff --git a/headers.md b/headers.md index bf8efb707..2ec700c4e 100644 --- a/headers.md +++ b/headers.md @@ -1,5 +1,177 @@ ---- -version_basis: { +# Security headers for your application + +Written against MDN HTTP documentation (no recorded version), nginx source release-1.26.3, Apache HTTP Server 2.4.69, Caddy source v2.11.4, Helmet v8.1.0, Django 5.2, and curl 8.23.0; 1 further source is pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 24 claims, all reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + +TLS protects the transport; these response headers protect the page. Set them at the proxy ([`add_header`](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L110-L116) in [nginx.md](nginx.md), [`Header`](https://github.com/apache/httpd/blob/2.4.69/docs/manual/mod/mod_headers.xml#L311-L328) in [apache.md](apache.md), [`header`](https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/headers/caddyfile.go#L27-L48) in Caddy), in app middleware (helmet for Express per [nodejs.md](nodejs.md), Django's security settings per [python.md](python.md)), or in a [`_headers` file](https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L14) for Cloudflare Pages static asset responses. Cloudflare Pages does not apply `_headers` to Pages Functions responses; set those headers in [Functions code](https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L26). Applying the three proxy directives to all six headers below is this guide's inference. + +## The set worth shipping + +``` +Strict-Transport-Security: max-age=31536000; includeSubDomains +Content-Security-Policy: default-src 'self' +X-Content-Type-Options: nosniff +Referrer-Policy: strict-origin-when-cross-origin +Permissions-Policy: camera=(), microphone=(), geolocation=() +X-Frame-Options: DENY +``` + +Notes that keep these correct rather than decorative: + +- **HSTS** only after HTTPS provably works everywhere on the domain; `includeSubDomains` commits every subdomain to HTTPS. Leave the `preload` token off unless you have read what preload-list inclusion means: removal is possible but slow (weeks, and browser-dependent as each browser ships an updated list), so treat inclusion as a long-lived commitment. +- **CSP** is the one that needs tailoring. Start from `default-src 'self'`, add the sources your app actually uses, and prefer nonces or hashes over `'unsafe-inline'` for scripts. Roll out with `Content-Security-Policy-Report-Only` first on an existing app so you see what would break before enforcing. +- **frame-ancestors** in CSP supersedes `X-Frame-Options` in browsers that support it. The modern equivalent of `X-Frame-Options: DENY` is `frame-ancestors 'none'` in the CSP; add it explicitly, because `default-src` does not cover framing. Sending both `X-Frame-Options` and `frame-ancestors` keeps older scanners content. +- Headers belong on every response, including error pages; this guide assesses setting them only on `200 /` as a common proxy misconfiguration. In nginx, `add_header` inherits the parent's headers only if a child has no header list ([merge](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L665-L690); [list creation](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L788-L796)), and [`always`](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L843-L849) covers statuses outside its default set ([status handling](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L216-L218); [nginx.md](nginx.md)). + +## Do not cache authenticated responses in a shared cache + +A CDN or shared proxy that keys its cache on the URL alone can serve one user's authenticated response to another. Set `Cache-Control: private` on authenticated responses, or `no-store` on the most sensitive ones, and mark cacheable only what is truly public. If a shared cache must hold authenticated content, configure it explicitly to vary on the cookie or the authorization header rather than relying on its default URL-only key. + +## Keep authenticated pages out of the browser's own cache + +`private` stops a shared cache from holding a response, but it still lets the user's own browser store it: MDN defines `private` as a response the browser's local cache may keep. So the Back button can redisplay an authenticated page after logout, and a chat transcript can sit in the history of a shared machine. Set `Cache-Control: no-store` on any response that carries authenticated content or sets a session cookie; MDN defines `no-store` as "any caches of any kind (private or shared) should not store this response", which covers the browser too. + +On logout, clear what the browser already holds. `Cache-Control: no-cache` does not cover this, because a history navigation such as the Back button can restore a back/forward-cache snapshot without revalidating. Send the `Clear-Site-Data` header on the logout-confirmation response instead: its `cache` directive clears the browser cache and, depending on the browser, the back/forward cache as well, and `cookies` clears the client-side session cookie (invalidate the server-side session separately, as part of logout). MDN's own logout example is `Clear-Site-Data: "cache", "cookies", "storage", "executionContexts", "prefetchCache", "prerenderCache"`. Know the scope before sending the full set: `"cookies"` clears cookies across the entire registered domain (all subdomains) and also clears HTTP authentication credentials, and `"storage"` removes origin local/session storage, IndexedDB, and service-worker registrations, so a shared parent domain or a companion app on a subdomain is affected too. + +## Verify + +REASONED: final-response header inspection follows the cited MDN header documentation; curl 8.23.0 documents the command options and write-out fields, while `-D -` printing each response's header block, and selecting the last one, are this guide's assessment. No deployment URL was supplied for live checks, and this guide records no run; the following block states the expected values and treats failed fetches as inconclusive. + +```bash +# -q (first) skips curlrc, -g disables URL globbing, and -sS hides progress but shows errors. +# Fetch the FINAL page and read its response headers by eye. -L follows redirects (--proto-redir +# '=https' blocks an https-to-http downgrade) so you inspect the page a browser lands on, not a 302; +# --noproxy so no client proxy rewrites the headers; the body is discarded. -D - prints EVERY +# response's headers (a 103 Early Hints and each redirect hop, then the final one), and -w prints where +# you ended up, so read the block under the LAST "HTTP/.. " line: +curl -q -g -sS -L --proto-redir '=https' --noproxy '*' -D - -o /dev/null -w 'final: %{http_code} %{url_effective}\n' https://example.com/ +# In that final response check VALUES, not just presence: Strict-Transport-Security with a large, +# non-zero max-age (max-age=0 disables HSTS); an ENFORCING Content-Security-Policy, not only +# Content-Security-Policy-Report-Only (which enforces nothing); X-Content-Type-Options nosniff; +# Referrer-Policy strict-origin-when-cross-origin; Permissions-Policy denying camera, microphone, and +# geolocation; and an effective deny-framing policy: CSP frame-ancestors 'none', or X-Frame-Options +# DENY with NO enforcing frame-ancestors overriding it (an enforcing frame-ancestors always wins over +# X-Frame-Options, so frame-ancestors * with X-Frame-Options: DENY still allows framing). A header +# missing from the final response, or present only on an earlier hop, is a finding; a failed fetch is +# inconclusive. +``` + +REASONED: preceding block; final-response header values, external scanning and CSP discrimination, per the guide's cited MDN HTTP headers reference and Security header scanner. No deployed application or controlled browser application session is available, and network access is unavailable. bash -n and ShellCheck 0.11.0 exited 0 without diagnostics; no HTTP response or browser outcome was observed. Exposed responses omit effective controls or allow the forbidden resource; fixed final responses carry the stated values and block the forbidden resource while permitting the allowed control. Then scan with https://securityheaders.com/ from outside for a second opinion (it fetches with GET). No UNEXPECTED CSP console violations during normal application use is necessary but not sufficient: a permissive CSP produces none, and a correct policy still logs a violation when it blocks something (the negative test below, an attack, or an unwanted load). Confirm the policy actually blocks by loading a page with a deliberately disallowed resource (an inline script the policy forbids, or an off-origin image) and checking it is refused while an allowed resource still loads. + +REASONED: shared-cache caller separation, per the guide's cited MDN Cache-Control. No deployed shared cache or authenticated A/B accounts are available; this prose supplies no local command or configuration to execute. No response sequence was observed. Exposed caching replays a previous caller's private content; fixed responses after warming the same URL belong only to their current authenticated or anonymous caller. For an authenticated route behind a shared cache, request the same URL as user A, then as user B, then anonymously, after warming the cache; each response must reflect only its own caller, never the one before it. + +REASONED: logout, browser history and response-header inspection, per the guide's cited MDN Cache-Control, MDN Clear-Site-Data and MDN back/forward cache. No application session or supported-browser deployment matrix is available; this prose supplies no local command or configuration to execute. No logout sequence was observed. Exposed history restores authenticated content after logout; the expected fixed outcome is a new request reaching login, with no-store and Clear-Site-Data checked independently in every supported browser. In a real browser, log in, open an authenticated page, then log out and press the Back button: the authenticated page must not reappear from history. The browser should re-request it and land on the login screen. That confirms the logout outcome, not which header produced it, so also inspect the authenticated and logout responses for the intended `no-store` and `Clear-Site-Data` headers. Because the back/forward-cache clearing is browser-dependent, run the check in each browser you support. + +## Sources (checked September 2026) + +- MDN HTTP headers reference (Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, X-Frame-Options): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers +- MDN Cache-Control (`no-store` forbids any cache including the browser; `private` still permits the browser's local cache): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control +- MDN Clear-Site-Data (the `cache` directive clears the browser cache and, depending on the browser, the back/forward cache; the logout example): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Clear-Site-Data +- MDN back/forward cache (a history navigation restores a snapshot without revalidating): https://developer.mozilla.org/en-US/docs/Glossary/bfcache +- Security header scanner: https://securityheaders.com/ +- Helmet v8.1.0 response-header middleware (checked October 2026): https://github.com/helmetjs/helmet/blob/v8.1.0/README.md#L3 +- Django 5.2 SecurityMiddleware (checked October 2026): https://docs.djangoproject.com/en/5.2/ref/middleware/#module-django.middleware.security +- nginx release-1.26.3 add_header directive (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L110-L116 +- nginx release-1.26.3 add_header list creation (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L788-L796 +- nginx release-1.26.3 header inheritance (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L665-L690 +- nginx release-1.26.3 always status handling (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L216-L218 +- nginx release-1.26.3 always option (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L843-L849 +- Apache 2.4.69 Header response-header directive (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/docs/manual/mod/mod_headers.xml#L311-L328 +- Caddy v2.11.4 header response-header directive (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/headers/caddyfile.go#L27-L48 +- Cloudflare Pages _headers on static assets (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L14 +- Cloudflare Pages _headers Pages Functions limit (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L26 +- curl 8.23.0 --disable (first argument skips curlrc) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/disable.md#L18-L20 +- curl 8.23.0 --globoff (disables URL globbing) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/globoff.md#L19-L22 +- curl 8.23.0 --silent (quiet mode with --show-error) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/silent.md#L20-L25 +- curl 8.23.0 --show-error (reports errors with --silent) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/show-error.md#L19 +- curl 8.23.0 --location (follows redirects) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/location.md#L23-L26 +- curl 8.23.0 --proto-redir (restricts redirect protocols) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/proto-redir.md#L19-L20 +- curl 8.23.0 --proto (= permits only the named protocol) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/proto.md#L30-L33 +- curl 8.23.0 --noproxy (* disables proxy use) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/noproxy.md#L18-L23 +- curl 8.23.0 --dump-header (- writes headers to stdout) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/dump-header.md#L21-L23 +- curl 8.23.0 --output (/dev/null discards the body) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/output.md#L53-L55 +- curl 8.23.0 --write-out (format and variables) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L20-L29 +- curl 8.23.0 --write-out http_code (last response code) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L112-L114 +- curl 8.23.0 --write-out url_effective (last fetched URL) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L358-L360 +- curl 8.23.0 --disable short form -q (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/disable.md#L5 +- curl 8.23.0 --globoff short form -g (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/globoff.md#L5 +- curl 8.23.0 --silent short form -s (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/silent.md#L5 +- curl 8.23.0 --show-error short form -S (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/show-error.md#L5 +- curl 8.23.0 --location short form -L (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/location.md#L5 +- curl 8.23.0 --dump-header short form -D (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/dump-header.md#L5 +- curl 8.23.0 --output short form -o (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/output.md#L6 +- curl 8.23.0 --write-out short form -w (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L5 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/headers.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- mdn: MDN HTTP documentation (unknown) +- nginx-source: nginx source (release-1.26.3) +- apache-source: Apache HTTP Server (2.4.69) +- caddy-source: Caddy source (v2.11.4) +- cloudflare-pages: Cloudflare Pages documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- helmet: Helmet (v8.1.0) +- django: Django (5.2) +- scanner: Security Headers scanner (unknown) +- curl: curl (8.23.0) + +Claims (full source mapping in the JSON block below): + +- placement (REASONED; mdn, helmet, django, nginx-source, apache-source, caddy-source, cloudflare-pages): Set response headers at a proxy with nginx add_header, Apache Header or Caddy header, in application middleware, or in a Cloudflare Pages _headers file for static asset responses; Pages Functions responses will likely need headers set in Functions code. Applying the three proxy directives to all six listed headers is this guide's inference. +- hsts (REASONED; mdn): Use HSTS max-age=31536000 with includeSubDomains only after HTTPS works across the domain; every subdomain is committed to HTTPS and max-age=0 disables HSTS. +- hsts-preload (REASONED; mdn): Omit preload unless its commitment is understood; removal is slow, takes weeks and depends on browser list updates. +- csp (REASONED; mdn): Start CSP at default-src 'self', tailor sources to the app and prefer script nonces or hashes over unsafe-inline; Verify requires an enforcing policy. +- csp-report-only (REASONED; mdn): Roll out Content-Security-Policy-Report-Only first on existing apps to find breakage; it enforces nothing and is not a substitute for enforcing CSP. +- nosniff (REASONED; mdn): Send X-Content-Type-Options: nosniff and verify that value on the final response. +- referrer (REASONED; mdn): Send Referrer-Policy: strict-origin-when-cross-origin and verify that final-response value. +- permissions (REASONED; mdn): Permissions-Policy disables camera, microphone and geolocation with empty allowlists; verify all three restrictions. +- framing (REASONED; mdn): X-Frame-Options: DENY or enforcing CSP frame-ancestors 'none' denies framing; supported frame-ancestors supersedes X-Frame-Options, and default-src does not cover it. frame-ancestors * overrides DENY. +- all-responses (REASONED; mdn, nginx-source): Send headers on every response, including errors; nginx add_header inherits parent headers only when the child has no header list, and always covers statuses outside its default set. This guide assesses setting headers only on 200 / as a common proxy misconfiguration. +- shared-cache (REASONED; mdn): Use Cache-Control: private for authenticated responses or no-store for sensitive ones; cache only public content unless authenticated caching is explicitly configured. +- cache-key (REASONED; mdn): A URL-only shared cache can leak one user's response to another; explicitly vary authenticated cache entries on the cookie or authorization header. +- browser-cache (REASONED; mdn): private still permits browser storage; no-store forbids private and shared storage and belongs on authenticated responses and responses setting session cookies. +- history-cache (REASONED; mdn): no-cache does not prevent a Back navigation from restoring a back/forward-cache snapshot without revalidation. +- logout-cache (REASONED; mdn): Send Clear-Site-Data on logout confirmation; cache clears browser cache and may clear back/forward cache depending on the browser. +- logout-cookies (REASONED; mdn): Clear-Site-Data cookies clears cookies across the registered domain and all subdomains, plus HTTP authentication credentials; invalidate the server session separately. +- logout-storage (REASONED; mdn): Clear-Site-Data storage removes origin local/session storage, IndexedDB and service-worker registrations; understand effects on companion applications before clearing. +- logout-header (REASONED; mdn): The cited MDN logout example includes cache, cookies, storage, executionContexts, prefetchCache and prerenderCache. +- verify-fetch (REASONED; mdn, curl): Fetch the final page with redirects restricted to HTTPS, bypass client proxies, discard the body and inspect the last response's headers and final status/URL; curl 8.23.0 documents the command options and write-out fields, while `-D -` printing each response's header block, and selecting the last one, are this guide's assessment. +- verify-values (REASONED; mdn): Judge header values on the final response, not presence on an earlier hop; missing final headers are findings and a failed fetch is inconclusive. +- verify-scanner (REASONED; scanner): Use the external Security Headers scanner for a second opinion; the guide states that it fetches with GET. +- verify-csp (REASONED; mdn): Unexpected console violations must be absent in normal use, but this alone is insufficient; confirm a deliberately disallowed resource is blocked while an allowed one loads. +- verify-cache (REASONED; mdn): Warm an authenticated URL in the shared cache, then request it as user A, user B and anonymously; each response must belong only to its caller. +- verify-logout (REASONED; mdn): After logout, Back must not restore authenticated content; verify a request to the login screen and inspect no-store and Clear-Site-Data separately in every supported browser. + +````json +{ "schema": 1, "checked": "2026-10-17", "documentation_checked": "2026-09", @@ -48,13 +220,6 @@ version_basis: { "sba79d4023eca": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L26" } }, - "scanner": { - "name": "Security Headers scanner", - "basis": "unknown", - "sources": { - "s7a94d0fcb6ad": "https://securityheaders.com/" - } - }, "helmet": { "name": "Helmet", "basis": "v8.1.0", @@ -69,6 +234,13 @@ version_basis: { "se79954c08919": "https://docs.djangoproject.com/en/5.2/ref/middleware/#module-django.middleware.security" } }, + "scanner": { + "name": "Security Headers scanner", + "basis": "unknown", + "sources": { + "s7a94d0fcb6ad": "https://securityheaders.com/" + } + }, "curl": { "name": "curl", "basis": "8.23.0", @@ -124,151 +296,5 @@ version_basis: { "verify-logout": {"text": "After logout, Back must not restore authenticated content; verify a request to the login screen and inspect no-store and Clear-Site-Data separately in every supported browser.", "components": ["mdn"], "sources": ["mdn:s9166d803cf3a", "mdn:s7f290c45ad37", "mdn:s46957d9dc1d5"], "status": "REASONED"} } } ---- -# Security headers for your application - - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-17; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- mdn: MDN HTTP documentation (unknown) -- nginx-source: nginx source (release-1.26.3) -- apache-source: Apache HTTP Server (2.4.69) -- caddy-source: Caddy source (v2.11.4) -- cloudflare-pages: Cloudflare Pages documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- scanner: Security Headers scanner (unknown) -- helmet: Helmet (v8.1.0) -- django: Django (5.2) -- curl: curl (8.23.0) - -Claims (full source mapping in the front matter): - -- placement (REASONED; mdn, helmet, django, nginx-source, apache-source, caddy-source, cloudflare-pages): Set response headers at a proxy with nginx add_header, Apache Header or Caddy header, in application middleware, or in a Cloudflare Pages _headers file for static asset responses; Pages Functions responses will likely need headers set in Functions code. Applying the three proxy directives to all six listed headers is this guide's inference. -- hsts (REASONED; mdn): Use HSTS max-age=31536000 with includeSubDomains only after HTTPS works across the domain; every subdomain is committed to HTTPS and max-age=0 disables HSTS. -- hsts-preload (REASONED; mdn): Omit preload unless its commitment is understood; removal is slow, takes weeks and depends on browser list updates. -- csp (REASONED; mdn): Start CSP at default-src 'self', tailor sources to the app and prefer script nonces or hashes over unsafe-inline; Verify requires an enforcing policy. -- csp-report-only (REASONED; mdn): Roll out Content-Security-Policy-Report-Only first on existing apps to find breakage; it enforces nothing and is not a substitute for enforcing CSP. -- nosniff (REASONED; mdn): Send X-Content-Type-Options: nosniff and verify that value on the final response. -- referrer (REASONED; mdn): Send Referrer-Policy: strict-origin-when-cross-origin and verify that final-response value. -- permissions (REASONED; mdn): Permissions-Policy disables camera, microphone and geolocation with empty allowlists; verify all three restrictions. -- framing (REASONED; mdn): X-Frame-Options: DENY or enforcing CSP frame-ancestors 'none' denies framing; supported frame-ancestors supersedes X-Frame-Options, and default-src does not cover it. frame-ancestors * overrides DENY. -- all-responses (REASONED; mdn, nginx-source): Send headers on every response, including errors; nginx add_header inherits parent headers only when the child has no header list, and always covers statuses outside its default set. This guide assesses setting headers only on 200 / as a common proxy misconfiguration. -- shared-cache (REASONED; mdn): Use Cache-Control: private for authenticated responses or no-store for sensitive ones; cache only public content unless authenticated caching is explicitly configured. -- cache-key (REASONED; mdn): A URL-only shared cache can leak one user's response to another; explicitly vary authenticated cache entries on the cookie or authorization header. -- browser-cache (REASONED; mdn): private still permits browser storage; no-store forbids private and shared storage and belongs on authenticated responses and responses setting session cookies. -- history-cache (REASONED; mdn): no-cache does not prevent a Back navigation from restoring a back/forward-cache snapshot without revalidation. -- logout-cache (REASONED; mdn): Send Clear-Site-Data on logout confirmation; cache clears browser cache and may clear back/forward cache depending on the browser. -- logout-cookies (REASONED; mdn): Clear-Site-Data cookies clears cookies across the registered domain and all subdomains, plus HTTP authentication credentials; invalidate the server session separately. -- logout-storage (REASONED; mdn): Clear-Site-Data storage removes origin local/session storage, IndexedDB and service-worker registrations; understand effects on companion applications before clearing. -- logout-header (REASONED; mdn): The cited MDN logout example includes cache, cookies, storage, executionContexts, prefetchCache and prerenderCache. -- verify-fetch (REASONED; mdn, curl): Fetch the final page with redirects restricted to HTTPS, bypass client proxies, discard the body and inspect the last response's headers and final status/URL; curl 8.23.0 documents the command options and write-out fields, while `-D -` printing each response's header block, and selecting the last one, are this guide's assessment. -- verify-values (REASONED; mdn): Judge header values on the final response, not presence on an earlier hop; missing final headers are findings and a failed fetch is inconclusive. -- verify-scanner (REASONED; scanner): Use the external Security Headers scanner for a second opinion; the guide states that it fetches with GET. -- verify-csp (REASONED; mdn): Unexpected console violations must be absent in normal use, but this alone is insufficient; confirm a deliberately disallowed resource is blocked while an allowed one loads. -- verify-cache (REASONED; mdn): Warm an authenticated URL in the shared cache, then request it as user A, user B and anonymously; each response must belong only to its caller. -- verify-logout (REASONED; mdn): After logout, Back must not restore authenticated content; verify a request to the login screen and inspect no-store and Clear-Site-Data separately in every supported browser. +```` - -TLS protects the transport; these response headers protect the page. Set them at the proxy ([`add_header`](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L110-L116) in [nginx.md](nginx.md), [`Header`](https://github.com/apache/httpd/blob/2.4.69/docs/manual/mod/mod_headers.xml#L311-L328) in [apache.md](apache.md), [`header`](https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/headers/caddyfile.go#L27-L48) in Caddy), in app middleware (helmet for Express per [nodejs.md](nodejs.md), Django's security settings per [python.md](python.md)), or in a [`_headers` file](https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L14) for Cloudflare Pages static asset responses. Cloudflare Pages does not apply `_headers` to Pages Functions responses; set those headers in [Functions code](https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L26). Applying the three proxy directives to all six headers below is this guide's inference. - -## The set worth shipping - -``` -Strict-Transport-Security: max-age=31536000; includeSubDomains -Content-Security-Policy: default-src 'self' -X-Content-Type-Options: nosniff -Referrer-Policy: strict-origin-when-cross-origin -Permissions-Policy: camera=(), microphone=(), geolocation=() -X-Frame-Options: DENY -``` - -Notes that keep these correct rather than decorative: - -- **HSTS** only after HTTPS provably works everywhere on the domain; `includeSubDomains` commits every subdomain to HTTPS. Leave the `preload` token off unless you have read what preload-list inclusion means: removal is possible but slow (weeks, and browser-dependent as each browser ships an updated list), so treat inclusion as a long-lived commitment. -- **CSP** is the one that needs tailoring. Start from `default-src 'self'`, add the sources your app actually uses, and prefer nonces or hashes over `'unsafe-inline'` for scripts. Roll out with `Content-Security-Policy-Report-Only` first on an existing app so you see what would break before enforcing. -- **frame-ancestors** in CSP supersedes `X-Frame-Options` in browsers that support it. The modern equivalent of `X-Frame-Options: DENY` is `frame-ancestors 'none'` in the CSP; add it explicitly, because `default-src` does not cover framing. Sending both `X-Frame-Options` and `frame-ancestors` keeps older scanners content. -- Headers belong on every response, including error pages; this guide assesses setting them only on `200 /` as a common proxy misconfiguration. In nginx, `add_header` inherits the parent's headers only if a child has no header list ([merge](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L665-L690); [list creation](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L788-L796)), and [`always`](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L843-L849) covers statuses outside its default set ([status handling](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L216-L218); [nginx.md](nginx.md)). - -## Do not cache authenticated responses in a shared cache - -A CDN or shared proxy that keys its cache on the URL alone can serve one user's authenticated response to another. Set `Cache-Control: private` on authenticated responses, or `no-store` on the most sensitive ones, and mark cacheable only what is truly public. If a shared cache must hold authenticated content, configure it explicitly to vary on the cookie or the authorization header rather than relying on its default URL-only key. - -## Keep authenticated pages out of the browser's own cache - -`private` stops a shared cache from holding a response, but it still lets the user's own browser store it: MDN defines `private` as a response the browser's local cache may keep. So the Back button can redisplay an authenticated page after logout, and a chat transcript can sit in the history of a shared machine. Set `Cache-Control: no-store` on any response that carries authenticated content or sets a session cookie; MDN defines `no-store` as "any caches of any kind (private or shared) should not store this response", which covers the browser too. - -On logout, clear what the browser already holds. `Cache-Control: no-cache` does not cover this, because a history navigation such as the Back button can restore a back/forward-cache snapshot without revalidating. Send the `Clear-Site-Data` header on the logout-confirmation response instead: its `cache` directive clears the browser cache and, depending on the browser, the back/forward cache as well, and `cookies` clears the client-side session cookie (invalidate the server-side session separately, as part of logout). MDN's own logout example is `Clear-Site-Data: "cache", "cookies", "storage", "executionContexts", "prefetchCache", "prerenderCache"`. Know the scope before sending the full set: `"cookies"` clears cookies across the entire registered domain (all subdomains) and also clears HTTP authentication credentials, and `"storage"` removes origin local/session storage, IndexedDB, and service-worker registrations, so a shared parent domain or a companion app on a subdomain is affected too. - -## Verify - -REASONED: final-response header inspection follows the cited MDN header documentation; curl 8.23.0 documents the command options and write-out fields, while `-D -` printing each response's header block, and selecting the last one, are this guide's assessment. No deployment URL was supplied for live checks, and this guide records no run; the following block states the expected values and treats failed fetches as inconclusive. - -```bash -# -q (first) skips curlrc, -g disables URL globbing, and -sS hides progress but shows errors. -# Fetch the FINAL page and read its response headers by eye. -L follows redirects (--proto-redir -# '=https' blocks an https-to-http downgrade) so you inspect the page a browser lands on, not a 302; -# --noproxy so no client proxy rewrites the headers; the body is discarded. -D - prints EVERY -# response's headers (a 103 Early Hints and each redirect hop, then the final one), and -w prints where -# you ended up, so read the block under the LAST "HTTP/.. " line: -curl -q -g -sS -L --proto-redir '=https' --noproxy '*' -D - -o /dev/null -w 'final: %{http_code} %{url_effective}\n' https://example.com/ -# In that final response check VALUES, not just presence: Strict-Transport-Security with a large, -# non-zero max-age (max-age=0 disables HSTS); an ENFORCING Content-Security-Policy, not only -# Content-Security-Policy-Report-Only (which enforces nothing); X-Content-Type-Options nosniff; -# Referrer-Policy strict-origin-when-cross-origin; Permissions-Policy denying camera, microphone, and -# geolocation; and an effective deny-framing policy: CSP frame-ancestors 'none', or X-Frame-Options -# DENY with NO enforcing frame-ancestors overriding it (an enforcing frame-ancestors always wins over -# X-Frame-Options, so frame-ancestors * with X-Frame-Options: DENY still allows framing). A header -# missing from the final response, or present only on an earlier hop, is a finding; a failed fetch is -# inconclusive. -``` - -REASONED: preceding block; final-response header values, external scanning and CSP discrimination, per the guide's cited MDN HTTP headers reference and Security header scanner. No deployed application or controlled browser application session is available, and network access is unavailable. bash -n and ShellCheck 0.11.0 exited 0 without diagnostics; no HTTP response or browser outcome was observed. Exposed responses omit effective controls or allow the forbidden resource; fixed final responses carry the stated values and block the forbidden resource while permitting the allowed control. Then scan with https://securityheaders.com/ from outside for a second opinion (it fetches with GET). No UNEXPECTED CSP console violations during normal application use is necessary but not sufficient: a permissive CSP produces none, and a correct policy still logs a violation when it blocks something (the negative test below, an attack, or an unwanted load). Confirm the policy actually blocks by loading a page with a deliberately disallowed resource (an inline script the policy forbids, or an off-origin image) and checking it is refused while an allowed resource still loads. - -REASONED: shared-cache caller separation, per the guide's cited MDN Cache-Control. No deployed shared cache or authenticated A/B accounts are available; this prose supplies no local command or configuration to execute. No response sequence was observed. Exposed caching replays a previous caller's private content; fixed responses after warming the same URL belong only to their current authenticated or anonymous caller. For an authenticated route behind a shared cache, request the same URL as user A, then as user B, then anonymously, after warming the cache; each response must reflect only its own caller, never the one before it. - -REASONED: logout, browser history and response-header inspection, per the guide's cited MDN Cache-Control, MDN Clear-Site-Data and MDN back/forward cache. No application session or supported-browser deployment matrix is available; this prose supplies no local command or configuration to execute. No logout sequence was observed. Exposed history restores authenticated content after logout; the expected fixed outcome is a new request reaching login, with no-store and Clear-Site-Data checked independently in every supported browser. In a real browser, log in, open an authenticated page, then log out and press the Back button: the authenticated page must not reappear from history. The browser should re-request it and land on the login screen. That confirms the logout outcome, not which header produced it, so also inspect the authenticated and logout responses for the intended `no-store` and `Clear-Site-Data` headers. Because the back/forward-cache clearing is browser-dependent, run the check in each browser you support. - -## Sources (checked September 2026) - -- MDN HTTP headers reference (Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, X-Frame-Options): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers -- MDN Cache-Control (`no-store` forbids any cache including the browser; `private` still permits the browser's local cache): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control -- MDN Clear-Site-Data (the `cache` directive clears the browser cache and, depending on the browser, the back/forward cache; the logout example): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Clear-Site-Data -- MDN back/forward cache (a history navigation restores a snapshot without revalidating): https://developer.mozilla.org/en-US/docs/Glossary/bfcache -- Security header scanner: https://securityheaders.com/ -- Helmet v8.1.0 response-header middleware (checked October 2026): https://github.com/helmetjs/helmet/blob/v8.1.0/README.md#L3 -- Django 5.2 SecurityMiddleware (checked October 2026): https://docs.djangoproject.com/en/5.2/ref/middleware/#module-django.middleware.security -- nginx release-1.26.3 add_header directive (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L110-L116 -- nginx release-1.26.3 add_header list creation (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L788-L796 -- nginx release-1.26.3 header inheritance (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L665-L690 -- nginx release-1.26.3 always status handling (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L216-L218 -- nginx release-1.26.3 always option (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L843-L849 -- Apache 2.4.69 Header response-header directive (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/docs/manual/mod/mod_headers.xml#L311-L328 -- Caddy v2.11.4 header response-header directive (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/headers/caddyfile.go#L27-L48 -- Cloudflare Pages _headers on static assets (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L14 -- Cloudflare Pages _headers Pages Functions limit (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L26 -- curl 8.23.0 --disable (first argument skips curlrc) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/disable.md#L18-L20 -- curl 8.23.0 --globoff (disables URL globbing) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/globoff.md#L19-L22 -- curl 8.23.0 --silent (quiet mode with --show-error) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/silent.md#L20-L25 -- curl 8.23.0 --show-error (reports errors with --silent) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/show-error.md#L19 -- curl 8.23.0 --location (follows redirects) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/location.md#L23-L26 -- curl 8.23.0 --proto-redir (restricts redirect protocols) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/proto-redir.md#L19-L20 -- curl 8.23.0 --proto (= permits only the named protocol) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/proto.md#L30-L33 -- curl 8.23.0 --noproxy (* disables proxy use) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/noproxy.md#L18-L23 -- curl 8.23.0 --dump-header (- writes headers to stdout) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/dump-header.md#L21-L23 -- curl 8.23.0 --output (/dev/null discards the body) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/output.md#L53-L55 -- curl 8.23.0 --write-out (format and variables) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L20-L29 -- curl 8.23.0 --write-out http_code (last response code) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L112-L114 -- curl 8.23.0 --write-out url_effective (last fetched URL) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L358-L360 -- curl 8.23.0 --disable short form -q (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/disable.md#L5 -- curl 8.23.0 --globoff short form -g (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/globoff.md#L5 -- curl 8.23.0 --silent short form -s (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/silent.md#L5 -- curl 8.23.0 --show-error short form -S (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/show-error.md#L5 -- curl 8.23.0 --location short form -L (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/location.md#L5 -- curl 8.23.0 --dump-header short form -D (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/dump-header.md#L5 -- curl 8.23.0 --output short form -o (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/output.md#L6 -- curl 8.23.0 --write-out short form -w (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L5 diff --git a/kubernetes.md b/kubernetes.md index 8306ecb13..d92220635 100644 --- a/kubernetes.md +++ b/kubernetes.md @@ -1,311 +1,16 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-27", - "documentation_checked": "2026-09", - "body_sha256": "1e80b5ac6d9df2701bae1929db0d6cb2f9ce6416a58415b189d509640b8a44c8", - "components": { - "kubernetes": { - "name": "Kubernetes documentation", - "basis": "unknown", - "sources": { - "s0f11d6ed39b7": "https://kubernetes.io/blog/2026/01/29/ingress-nginx-statement/", - "sca23283c5a9b": "https://kubernetes.io/docs/concepts/services-networking/gateway/", - "sd8c8df3b9ddc": "https://kubernetes.io/docs/concepts/services-networking/ingress/", - "se41a53ea6778": "https://kubernetes.io/docs/reference/config-api/kubeconfig.v1/", - "sae633285120e": "https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/", - "sd59fb2f35cd7": "https://kubernetes.io/docs/reference/networking/ports-and-protocols/", - "s01f081fed0ff": "https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/", - "s49a29258961b": "https://kubernetes.io/docs/reference/access-authn-authz/kubelet-authn-authz/", - "s04f0c3099fd7": "https://kubernetes.io/docs/reference/kubectl/jsonpath/" - } - }, - "kubelet": { - "name": "Kubernetes kubelet/kubeadm source", - "basis": "v1.37.1", - "sources": { - "sc6056d6413a0": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L196-L224", - "s227d2a259fd3": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L373-L398", - "s568cd52cc674": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/apis/config/v1beta1/defaults.go#L85-L102", - "s2daf96dabbdf": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L281", - "s24ba323c3dab": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L282", - "sd81692a1876f": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L33-L48", - "s0eb30acf2f46": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L95-L100", - "s1c86aaef7307": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L144-L188", - "sb98bd327db92": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/utils.go#L66-L71", - "s3fc405855b4a": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/staging/src/k8s.io/kubelet/config/v1beta1/types.go#L160-L176", - "s98cf98130201": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L181-L250", - "s8bea69523ed9": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L339-L375", - "sa02a895a475c": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/cluster/ports/ports.go#L32-L37", - "s6b753b247f8a": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L477-L572", - "sd00d4cdb9276": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/stats/handler.go#L109-L133", - "s02441dcae3cf": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L577-L610" - } - }, - "envoy": { - "name": "Envoy Gateway", - "basis": "v1.9.1", - "sources": { - "s6412abbdfdbd": "https://github.com/envoyproxy/gateway/releases/download/v1.9.1/quickstart.yaml" - } - }, - "gateway": { - "name": "Gateway API documentation", - "basis": "unknown", - "sources": { - "se0e45c61dc65": "https://gateway-api.sigs.k8s.io/guides/getting-started/introduction/", - "se1f28d98d168": "https://gateway-api.sigs.k8s.io/guides/user-guides/tls/", - "s4a971425e1d1": "https://gateway-api.sigs.k8s.io/guides/user-guides/http-routing/", - "s8d02fbc742ba": "https://gateway-api.sigs.k8s.io/guides/user-guides/http-redirect-rewrite/" - } - }, - "cert-manager": { - "name": "cert-manager Gateway support minimum", - "basis": "1.15", - "sources": { - "s72bdb97928a8": "https://cert-manager.io/docs/usage/gateway/", - "s8d82244c2d6d": "https://cert-manager.io/docs/configuration/acme/http01/" - } - }, - "apache": { - "name": "Apache htpasswd documentation", - "basis": "unknown", - "sources": { - "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" - } - }, - "authelia": { - "name": "Authelia documentation", - "basis": "unknown", - "sources": { - "s0cb4f49871b5": "https://www.authelia.com/integration/proxies/introduction/", - "scfa9e2cc2ef5": "https://www.authelia.com/integration/kubernetes/envoy/gateway/" - } - }, - "eks": { - "name": "Amazon EKS documentation", - "basis": "unknown", - "sources": { - "s9724ce882930": "https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html" - } - }, - "eks-ami": { - "name": "Amazon EKS AMI bootstrap", - "basis": "6caf8311a3c6a8da71ac7e5e83f9c2e06287039a", - "sources": { - "sbe1fce93c7b2": "https://github.com/awslabs/amazon-eks-ami/blob/6caf8311a3c6a8da71ac7e5e83f9c2e06287039a/nodeadm/internal/kubelet/config.go" - } - }, - "gke": { - "name": "GKE documentation", - "basis": "unknown", - "sources": { - "s0f44d89575ed": "https://docs.cloud.google.com/kubernetes-engine/docs/concepts/network-isolation#how_authorized_networks_work" - } - }, - "gke-min": { - "name": "GKE new-cluster read-only-port boundary", - "basis": "1.32", - "sources": { - "s4c7be4adcab4": "https://docs.cloud.google.com/kubernetes-engine/docs/how-to/disable-kubelet-readonly-port" - } - }, - "aks": { - "name": "AKS documentation", - "basis": "unknown", - "sources": { - "s897dd27ab9a1": "https://learn.microsoft.com/en-us/azure/aks/api-server-authorized-ip-ranges" - } - }, - "curl": { - "name": "curl documentation", - "basis": "unknown", - "sources": { - "s2b2686afaf41": "https://curl.se/docs/manpage.html", - "s7d4a6e627b1b": "https://curl.se/libcurl/c/libcurl-errors.html" - } - }, - "nmap": { - "name": "Nmap documentation", - "basis": "unknown", - "sources": { - "sb7d0e7eb5141": "https://nmap.org/book/man-host-discovery.html", - "s1818abeb3f92": "https://nmap.org/book/man-port-specification.html", - "s79c95712dd8d": "https://nmap.org/book/man-misc-options.html" - } - }, - "traefik": { - "name": "Traefik documentation", - "basis": "unknown", - "sources": { - "s06f2ee45331e": "https://doc.traefik.io/traefik/reference/install-configuration/providers/kubernetes/kubernetes-gateway/" - } - }, - "cilium": { - "name": "Cilium documentation", - "basis": "unknown", - "sources": { - "sacaa7a78461d": "https://docs.cilium.io/en/stable/network/servicemesh/gateway-api/gateway-api/" - } - }, - "envoy-rolling": { - "name": "Envoy Gateway documentation (rolling)", - "basis": "unknown", - "sources": { - "se833317feea7": "https://gateway.envoyproxy.io/docs/tasks/security/secure-gateways/", - "sb5fa4f4ffa1a": "https://gateway.envoyproxy.io/docs/tasks/security/basic-auth/", - "s7fdd76c5d549": "https://gateway.envoyproxy.io/docs/tasks/security/oidc/", - "s100049c74adf": "https://gateway.envoyproxy.io/docs/tasks/security/ext-auth/", - "s9b1d646cb718": "https://gateway.envoyproxy.io/docs/tasks/traffic/http-redirect/", - "s881f08174c54": "https://gateway.envoyproxy.io/v1.9/install/install-helm/", - "s9ad0f418b20b": "https://gateway.envoyproxy.io/v1.9/tasks/quickstart/" - } - } - }, - "claims": { - "ingress-migration": {"text": "ingress-nginx retired in March 2026 with no subsequent updates; detect its labeled pods and migrate. Gateway API is recommended and Ingress API is frozen.", "components": ["kubernetes"], "sources": ["kubernetes:s0f11d6ed39b7", "kubernetes:sca23283c5a9b", "kubernetes:sd8c8df3b9ddc"], "status": "REASONED"}, - "gateway-install": {"text": "Install Envoy Gateway chart v1.9.1 and its default Gateway CRDs; separately create GatewayClass eg with the documented controllerName and confirm Accepted=True.", "components": ["envoy", "gateway", "envoy-rolling"], "sources": ["envoy-rolling:s881f08174c54", "envoy:s6412abbdfdbd", "gateway:se0e45c61dc65"], "status": "REASONED"}, - "gateway-alternatives": {"text": "Traefik uses providers.kubernetesGateway; Cilium uses gatewayAPI.enabled=true and requires kube-proxy replacement.", "components": ["traefik", "cilium"], "sources": ["traefik:s06f2ee45331e", "cilium:sacaa7a78461d"], "status": "REASONED"}, - "gateway-tls": {"text": "Gateway HTTPS listener 443 terminates TLS using app-tls for app.example.com; HTTP 80 is for redirect and ACME challenges.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:se1f28d98d168", "envoy-rolling:se833317feea7"], "status": "REASONED"}, - "gateway-route": {"text": "HTTPRoute app attaches only to the HTTPS listener via sectionName and forwards to the ClusterIP app Service on port 80.", "components": ["gateway"], "sources": ["gateway:s4a971425e1d1"], "status": "REASONED"}, - "gateway-redirect": {"text": "The HTTP route uses RequestRedirect with scheme https and statusCode 301.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:s8d02fbc742ba", "envoy-rolling:s9b1d646cb718"], "status": "REASONED"}, - "cert-manager-enable": {"text": "From cert-manager 1.15, enable config.gatewayAPI.enabled; install Gateway CRDs before startup or restart cert-manager afterwards.", "components": ["cert-manager"], "sources": ["cert-manager:s72bdb97928a8"], "status": "REASONED"}, - "cert-manager-issue": {"text": "Gateway issuer annotations produce Certificates per HTTPS-listener Secret using listener hostnames; ACME gatewayHTTPRoute parentRefs selects the challenge Gateway.", "components": ["cert-manager"], "sources": ["cert-manager:s72bdb97928a8", "cert-manager:s8d82244c2d6d"], "status": "REASONED"}, - "basic-auth-policy": {"text": "Gateway API has no standard auth filter; Envoy SecurityPolicy attaches Basic auth to Gateway/HTTPRoute/GRPCRoute using an htpasswd Secret.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:s4a971425e1d1", "envoy-rolling:sb5fa4f4ffa1a"], "status": "REASONED"}, - "basic-auth-hash": {"text": "Envoy supports SHA hashes only; htpasswd -s is weak SHA-1, so use long random passwords over TLS and keep application login.", "components": ["apache", "envoy-rolling"], "sources": ["envoy-rolling:sb5fa4f4ffa1a", "apache:scde9004bb967"], "status": "REASONED"}, - "basic-auth-input": {"text": "htpasswd -i reads stdin, unlike argv-exposing -b; confirm twice, create with -cis, verify with -vi and only then create the Kubernetes Secret.", "components": ["apache", "envoy-rolling"], "sources": ["apache:scde9004bb967", "envoy-rolling:sb5fa4f4ffa1a"], "status": "REASONED"}, - "oidc": {"text": "Envoy SecurityPolicy oidc uses issuer, clientID, clientSecret and redirectURL; enforce MFA at the identity provider.", "components": ["envoy-rolling"], "sources": ["envoy-rolling:s7fdd76c5d549"], "status": "REASONED"}, - "external-auth": {"text": "Authelia is an authorization endpoint, not a traffic proxy; Envoy extAuth.http targets its Service and /api/authz/ext-authz/.", "components": ["authelia", "envoy-rolling"], "sources": ["authelia:s0cb4f49871b5", "authelia:scfa9e2cc2ef5", "envoy-rolling:s100049c74adf"], "status": "REASONED"}, - "workload-exposure": {"text": "Expose workloads only through the Gateway LoadBalancer; keep databases on ClusterIP without routes and enforce pod access with NetworkPolicies plus database TLS/auth.", "components": ["envoy-rolling", "kubernetes"], "sources": ["envoy-rolling:s9ad0f418b20b", "kubernetes:sca23283c5a9b"], "status": "REASONED"}, - "secrets": {"text": "Store credentials in Secrets or an external operator, not committed ConfigMaps or environment literals; kubeconfigs containing credentials also need secret handling.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778", "kubernetes:sae633285120e"], "status": "REASONED"}, - "eks-endpoint": {"text": "EKS API defaults public; restrict public CIDRs or use private access. Private-only endpoints can resolve publicly to private VPC addresses.", "components": ["eks"], "sources": ["eks:s9724ce882930"], "status": "REASONED"}, - "gke-endpoints": {"text": "GKE authorized networks constrain IP-based access, not its separately IAM-gated DNS endpoint; inspect both and disable unused DNS access.", "components": ["gke"], "sources": ["gke:s0f44d89575ed"], "status": "REASONED"}, - "aks-endpoint": {"text": "Restrict AKS API access with authorized IP ranges; workload Gateway protection does not secure the control plane.", "components": ["aks"], "sources": ["aks:s897dd27ab9a1"], "status": "REASONED"}, - "kubeconfig": {"text": "Embedded tokens or client keys confer their RBAC access without another factor; exec-plugin configs may be only pointers, but args/env can themselves hold credentials.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778"], "status": "REASONED"}, - "api-port": {"text": "API server commonly uses 6443, but providers may use 443; use the complete kubeconfig endpoint and its actual port.", "components": ["kubernetes", "eks"], "sources": ["kubernetes:sd59fb2f35cd7", "eks:s9724ce882930"], "status": "REASONED"}, - "etcd-ports": {"text": "etcd client/peer defaults are 2379/2380; require certificates, restrict holders and private access, and retain loopback listeners used by the local API server.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7", "kubernetes:s01f081fed0ff"], "status": "REASONED"}, - "etcd-storage": {"text": "API resources default to plaintext storage in etcd; valid client certificates are the boundary and etcd access is equivalent to cluster root.", "components": ["kubernetes"], "sources": ["kubernetes:sae633285120e", "kubernetes:s01f081fed0ff"], "status": "REASONED"}, - "scheduler-port": {"text": "Scheduler default 10259 must remain restricted to cluster clients.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, - "controller-port": {"text": "Controller-manager default 10257 must remain restricted to cluster clients.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, - "proxy-port": {"text": "Worker kube-proxy health default 10256 needs separate node exposure checks.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, - "nodeports": {"text": "NodePort defaults span 30000 to 32767 TCP/UDP; Service listings do not actively probe that range.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, - "kubelet-legacy-anonymous": {"text": "Legacy kubelet flags default anonymous-auth=true; explicitly disable it where flags control authentication.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0", "kubelet:s227d2a259fd3"], "status": "REASONED"}, - "kubelet-legacy-webhook": {"text": "Legacy authentication-token-webhook defaults false; explicitly enable it where flags configure authentication.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0"], "status": "REASONED"}, - "kubelet-legacy-authz": {"text": "Legacy authorization-mode defaults AlwaysAllow; set Webhook where flags configure authorization.", "components": ["kubelet", "kubernetes"], "sources": ["kubelet:sc6056d6413a0", "kubernetes:s49a29258961b"], "status": "REASONED"}, - "kubelet-file-anonymous": {"text": "v1beta1 KubeletConfiguration instead defaults authentication.anonymous.enabled=false.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, - "kubelet-file-webhook": {"text": "v1beta1 KubeletConfiguration defaults authentication.webhook.enabled=true.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, - "kubelet-file-authz": {"text": "v1beta1 KubeletConfiguration defaults authorization.mode=Webhook.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, - "kubelet-precedence": {"text": "Explicit flags override --config; --config-dir drop-ins override defaults and the file. Inspect all effective settings together.", "components": ["kubelet"], "sources": ["kubelet:s2daf96dabbdf", "kubelet:s24ba323c3dab"], "status": "REASONED"}, - "kubeadm": {"text": "kubeadm sets anonymous false, token webhook true, Webhook authorization and readOnlyPort 0; user overrides are preserved with warnings.", "components": ["kubelet"], "sources": ["kubelet:sd81692a1876f", "kubelet:s0eb30acf2f46", "kubelet:s1c86aaef7307", "kubelet:sb98bd327db92"], "status": "REASONED"}, - "eks-bootstrap": {"text": "The pinned EKS AMI bootstrap writes Anonymous.Enabled=false, Mode=Webhook and ReadOnlyPort=0; do not generalize to all managed nodes.", "components": ["eks-ami"], "sources": ["eks-ami:sbe1fce93c7b2"], "status": "REASONED"}, - "kubelet-bind": {"text": "Secured kubelet defaults to address 0.0.0.0 on 10250; wildcard help includes both families. Bind private addresses and restrict authorized callers.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674", "kubelet:s3fc405855b4a", "kubelet:s227d2a259fd3", "kubelet:s98cf98130201"], "status": "REASONED"}, - "kubelet-exec": {"text": "10250 debugging handlers can execute in containers; authentication does not justify public reachability.", "components": ["kubelet"], "sources": ["kubelet:s8bea69523ed9", "kubelet:s3fc405855b4a", "kubelet:s98cf98130201", "kubelet:s02441dcae3cf"], "status": "REASONED"}, - "readonly-default": {"text": "Legacy read-only port defaults 10255, while v1beta1 readOnlyPort defaults 0; explicitly disable it and remove enabling overrides.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0", "kubelet:sa02a895a475c", "kubelet:s3fc405855b4a"], "status": "REASONED"}, - "readonly-auth": {"text": "Read-only server uses plain HTTP with no authentication or authorization filter; its caller-supplied bind wiring was outside the inspected source subset.", "components": ["kubelet"], "sources": ["kubelet:s98cf98130201", "kubelet:s8bea69523ed9"], "status": "REASONED"}, - "readonly-paths": {"text": "Read-only handlers expose /pods, /stats/summary, metrics variants and health; v1.37.1 has no /spec handler. Securing 10250 does not secure 10255.", "components": ["kubelet"], "sources": ["kubelet:s6b753b247f8a", "kubelet:sd00d4cdb9276"], "status": "REASONED"}, - "gke-readonly": {"text": "GKE disables the read-only port by default only for new clusters running 1.32+; inspect effective configuration on older or upgraded clusters.", "components": ["gke-min"], "sources": ["gke-min:s4c7be4adcab4"], "status": "REASONED"}, - "verify-services": {"text": "Only the Gateway should have NodePort/LoadBalancer exposure; verify its public address/DNS and Certificate Ready=True.", "components": ["kubernetes", "envoy-rolling", "cert-manager"], "sources": ["kubernetes:s04f0c3099fd7", "envoy-rolling:s9ad0f418b20b", "cert-manager:s72bdb97928a8"], "status": "REASONED", "verify": [1]}, - "verify-redirect": {"text": "HTTP should return 301 to the HTTPS application URL.", "components": ["gateway"], "sources": ["gateway:s8d02fbc742ba"], "status": "REASONED", "verify": [1]}, - "verify-basic": {"text": "Anonymous Basic-protected access should return 401; require SecurityPolicy Accepted=True and a valid credential reaching the app without the gateway Basic challenge.", "components": ["curl", "envoy-rolling"], "sources": ["envoy-rolling:sb5fa4f4ffa1a", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, - "verify-api-target": {"text": "Use the whole kubeconfig endpoint, preserving IPv6 and inferring a schemeless endpoint from both cluster and user TLS settings, not a hardcoded 6443.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778", "kubernetes:sd59fb2f35cd7"], "status": "REASONED", "verify": [1]}, - "verify-proxies": {"text": "Disable curl config and configured proxies for direct probes; a transparent TLS middlebox can still answer for an unreachable target.", "components": ["curl"], "sources": ["curl:s2b2686afaf41", "curl:s7d4a6e627b1b"], "status": "REASONED", "verify": [1]}, - "verify-api-tcp": {"text": "Outside allowed ranges, nc success proves something accepted TCP; corroborate failed probes with provider ranges and multiple vantage points.", "components": ["kubernetes", "eks"], "sources": ["kubernetes:sd59fb2f35cd7", "eks:s9724ce882930"], "status": "REASONED", "verify": [1]}, - "verify-api-http": {"text": "Any HTTP status with curl exit 0 means answered; exit 60 means a TLS peer answered, 7 failed connect, 28 is ambiguous timeout and 6 is DNS failure, not privacy proof.", "components": ["curl", "eks"], "sources": ["curl:s7d4a6e627b1b", "eks:s9724ce882930"], "status": "REASONED", "verify": [1]}, - "verify-node-scan": {"text": "Scan control-plane hosts first and every public node address in both families; workers alone miss API/etcd and separate etcd hosts are absent from kubectl nodes.", "components": ["kubernetes", "nmap"], "sources": ["kubernetes:sd59fb2f35cd7", "kubernetes:s01f081fed0ff", "nmap:sb7d0e7eb5141", "nmap:s1818abeb3f92", "nmap:s79c95712dd8d"], "status": "REASONED", "verify": [1]}, - "verify-kubelet": {"text": "Untrusted clients should reach neither kubelet port after isolation, while authorized clients still reach 10250; readOnlyPort=0 should leave no 10255 listener even for authorized clients.", "components": ["kubelet"], "sources": ["kubelet:s98cf98130201", "kubelet:s3fc405855b4a"], "status": "REASONED", "verify": [1]} - } -} ---- # Kubernetes: Gateway API TLS and authentication - -**Version basis** +Written against Kubernetes kubelet/kubeadm source v1.37.1, Envoy Gateway v1.9.1, cert-manager Gateway support minimum 1.15, and GKE new-cluster read-only-port boundary 1.32; 1 further source is pinned to a commit and 12 carry no recorded version. Metadata reviewed 2026-09-27; documentation checked September 2026. The guide records 50 claims, all reasoned from cited sources. -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. -Metadata reviewed 2026-09-27; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +The cluster equivalents of this repository's rules: nothing reaches a workload except through the TLS-terminating entry point (a Gateway API `Gateway`), and no Service becomes public through a casual `type: LoadBalancer` or `NodePort`; the entry point's own Service is the only exception. -Components (recorded basis): +**If you run ingress-nginx today, migrate.** Earlier versions of this guide built on ingress-nginx. The Kubernetes project retired it in March 2026: per the Kubernetes Steering and Security Response Committees, "there will be no more releases for bug fixes, security patches, or any updates of any kind after the project is retired", and "choosing to remain with Ingress NGINX after its retirement leaves you and your users vulnerable to attack" (as of September 2026; statement linked in Sources). Detect it with cluster-admin permissions: `kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx`. Any pod returned means migration is required; the `nginx.ingress.kubernetes.io/*` annotations die with the controller. Kubernetes recommends Gateway API over Ingress (whose API is now frozen) and links a migration guide from its Gateway API page. The rest of this guide is the Gateway API form of the old rules. -- kubernetes: Kubernetes documentation (unknown) -- kubelet: Kubernetes kubelet/kubeadm source (v1.37.1) -- envoy: Envoy Gateway (v1.9.1) -- gateway: Gateway API documentation (unknown) -- cert-manager: cert-manager Gateway support minimum (1.15) -- apache: Apache htpasswd documentation (unknown) -- authelia: Authelia documentation (unknown) -- eks: Amazon EKS documentation (unknown) -- eks-ami: Amazon EKS AMI bootstrap (6caf8311a3c6a8da71ac7e5e83f9c2e06287039a) -- gke: GKE documentation (unknown) -- gke-min: GKE new-cluster read-only-port boundary (1.32) -- aks: AKS documentation (unknown) -- curl: curl documentation (unknown) -- nmap: Nmap documentation (unknown) -- traefik: Traefik documentation (unknown) -- cilium: Cilium documentation (unknown) -- envoy-rolling: Envoy Gateway documentation (rolling) (unknown) +## 1. Gateway API with a maintained implementation -Claims (full source mapping in the front matter): - -- ingress-migration (REASONED; kubernetes): ingress-nginx retired in March 2026 with no subsequent updates; detect its labeled pods and migrate. Gateway API is recommended and Ingress API is frozen. -- gateway-install (REASONED; envoy, gateway, envoy-rolling): Install Envoy Gateway chart v1.9.1 and its default Gateway CRDs; separately create GatewayClass eg with the documented controllerName and confirm Accepted=True. -- gateway-alternatives (REASONED; traefik, cilium): Traefik uses providers.kubernetesGateway; Cilium uses gatewayAPI.enabled=true and requires kube-proxy replacement. -- gateway-tls (REASONED; gateway, envoy-rolling): Gateway HTTPS listener 443 terminates TLS using app-tls for app.example.com; HTTP 80 is for redirect and ACME challenges. -- gateway-route (REASONED; gateway): HTTPRoute app attaches only to the HTTPS listener via sectionName and forwards to the ClusterIP app Service on port 80. -- gateway-redirect (REASONED; gateway, envoy-rolling): The HTTP route uses RequestRedirect with scheme https and statusCode 301. -- cert-manager-enable (REASONED; cert-manager): From cert-manager 1.15, enable config.gatewayAPI.enabled; install Gateway CRDs before startup or restart cert-manager afterwards. -- cert-manager-issue (REASONED; cert-manager): Gateway issuer annotations produce Certificates per HTTPS-listener Secret using listener hostnames; ACME gatewayHTTPRoute parentRefs selects the challenge Gateway. -- basic-auth-policy (REASONED; gateway, envoy-rolling): Gateway API has no standard auth filter; Envoy SecurityPolicy attaches Basic auth to Gateway/HTTPRoute/GRPCRoute using an htpasswd Secret. -- basic-auth-hash (REASONED; apache, envoy-rolling): Envoy supports SHA hashes only; htpasswd -s is weak SHA-1, so use long random passwords over TLS and keep application login. -- basic-auth-input (REASONED; apache, envoy-rolling): htpasswd -i reads stdin, unlike argv-exposing -b; confirm twice, create with -cis, verify with -vi and only then create the Kubernetes Secret. -- oidc (REASONED; envoy-rolling): Envoy SecurityPolicy oidc uses issuer, clientID, clientSecret and redirectURL; enforce MFA at the identity provider. -- external-auth (REASONED; authelia, envoy-rolling): Authelia is an authorization endpoint, not a traffic proxy; Envoy extAuth.http targets its Service and /api/authz/ext-authz/. -- workload-exposure (REASONED; envoy-rolling, kubernetes): Expose workloads only through the Gateway LoadBalancer; keep databases on ClusterIP without routes and enforce pod access with NetworkPolicies plus database TLS/auth. -- secrets (REASONED; kubernetes): Store credentials in Secrets or an external operator, not committed ConfigMaps or environment literals; kubeconfigs containing credentials also need secret handling. -- eks-endpoint (REASONED; eks): EKS API defaults public; restrict public CIDRs or use private access. Private-only endpoints can resolve publicly to private VPC addresses. -- gke-endpoints (REASONED; gke): GKE authorized networks constrain IP-based access, not its separately IAM-gated DNS endpoint; inspect both and disable unused DNS access. -- aks-endpoint (REASONED; aks): Restrict AKS API access with authorized IP ranges; workload Gateway protection does not secure the control plane. -- kubeconfig (REASONED; kubernetes): Embedded tokens or client keys confer their RBAC access without another factor; exec-plugin configs may be only pointers, but args/env can themselves hold credentials. -- api-port (REASONED; kubernetes, eks): API server commonly uses 6443, but providers may use 443; use the complete kubeconfig endpoint and its actual port. -- etcd-ports (REASONED; kubernetes): etcd client/peer defaults are 2379/2380; require certificates, restrict holders and private access, and retain loopback listeners used by the local API server. -- etcd-storage (REASONED; kubernetes): API resources default to plaintext storage in etcd; valid client certificates are the boundary and etcd access is equivalent to cluster root. -- scheduler-port (REASONED; kubernetes): Scheduler default 10259 must remain restricted to cluster clients. -- controller-port (REASONED; kubernetes): Controller-manager default 10257 must remain restricted to cluster clients. -- proxy-port (REASONED; kubernetes): Worker kube-proxy health default 10256 needs separate node exposure checks. -- nodeports (REASONED; kubernetes): NodePort defaults span 30000 to 32767 TCP/UDP; Service listings do not actively probe that range. -- kubelet-legacy-anonymous (REASONED; kubelet): Legacy kubelet flags default anonymous-auth=true; explicitly disable it where flags control authentication. -- kubelet-legacy-webhook (REASONED; kubelet): Legacy authentication-token-webhook defaults false; explicitly enable it where flags configure authentication. -- kubelet-legacy-authz (REASONED; kubelet, kubernetes): Legacy authorization-mode defaults AlwaysAllow; set Webhook where flags configure authorization. -- kubelet-file-anonymous (REASONED; kubelet): v1beta1 KubeletConfiguration instead defaults authentication.anonymous.enabled=false. -- kubelet-file-webhook (REASONED; kubelet): v1beta1 KubeletConfiguration defaults authentication.webhook.enabled=true. -- kubelet-file-authz (REASONED; kubelet): v1beta1 KubeletConfiguration defaults authorization.mode=Webhook. -- kubelet-precedence (REASONED; kubelet): Explicit flags override --config; --config-dir drop-ins override defaults and the file. Inspect all effective settings together. -- kubeadm (REASONED; kubelet): kubeadm sets anonymous false, token webhook true, Webhook authorization and readOnlyPort 0; user overrides are preserved with warnings. -- eks-bootstrap (REASONED; eks-ami): The pinned EKS AMI bootstrap writes Anonymous.Enabled=false, Mode=Webhook and ReadOnlyPort=0; do not generalize to all managed nodes. -- kubelet-bind (REASONED; kubelet): Secured kubelet defaults to address 0.0.0.0 on 10250; wildcard help includes both families. Bind private addresses and restrict authorized callers. -- kubelet-exec (REASONED; kubelet): 10250 debugging handlers can execute in containers; authentication does not justify public reachability. -- readonly-default (REASONED; kubelet): Legacy read-only port defaults 10255, while v1beta1 readOnlyPort defaults 0; explicitly disable it and remove enabling overrides. -- readonly-auth (REASONED; kubelet): Read-only server uses plain HTTP with no authentication or authorization filter; its caller-supplied bind wiring was outside the inspected source subset. -- readonly-paths (REASONED; kubelet): Read-only handlers expose /pods, /stats/summary, metrics variants and health; v1.37.1 has no /spec handler. Securing 10250 does not secure 10255. -- gke-readonly (REASONED; gke-min): GKE disables the read-only port by default only for new clusters running 1.32+; inspect effective configuration on older or upgraded clusters. -- verify-services (REASONED; kubernetes, envoy-rolling, cert-manager): Only the Gateway should have NodePort/LoadBalancer exposure; verify its public address/DNS and Certificate Ready=True. -- verify-redirect (REASONED; gateway): HTTP should return 301 to the HTTPS application URL. -- verify-basic (REASONED; curl, envoy-rolling): Anonymous Basic-protected access should return 401; require SecurityPolicy Accepted=True and a valid credential reaching the app without the gateway Basic challenge. -- verify-api-target (REASONED; kubernetes): Use the whole kubeconfig endpoint, preserving IPv6 and inferring a schemeless endpoint from both cluster and user TLS settings, not a hardcoded 6443. -- verify-proxies (REASONED; curl): Disable curl config and configured proxies for direct probes; a transparent TLS middlebox can still answer for an unreachable target. -- verify-api-tcp (REASONED; kubernetes, eks): Outside allowed ranges, nc success proves something accepted TCP; corroborate failed probes with provider ranges and multiple vantage points. -- verify-api-http (REASONED; curl, eks): Any HTTP status with curl exit 0 means answered; exit 60 means a TLS peer answered, 7 failed connect, 28 is ambiguous timeout and 6 is DNS failure, not privacy proof. -- verify-node-scan (REASONED; kubernetes, nmap): Scan control-plane hosts first and every public node address in both families; workers alone miss API/etcd and separate etcd hosts are absent from kubectl nodes. -- verify-kubelet (REASONED; kubelet): Untrusted clients should reach neither kubelet port after isolation, while authorized clients still reach 10250; readOnlyPort=0 should leave no 10255 listener even for authorized clients. - - -The cluster equivalents of this repository's rules: nothing reaches a workload except through the TLS-terminating entry point (a Gateway API `Gateway`), and no Service becomes public through a casual `type: LoadBalancer` or `NodePort`; the entry point's own Service is the only exception. - -**If you run ingress-nginx today, migrate.** Earlier versions of this guide built on ingress-nginx. The Kubernetes project retired it in March 2026: per the Kubernetes Steering and Security Response Committees, "there will be no more releases for bug fixes, security patches, or any updates of any kind after the project is retired", and "choosing to remain with Ingress NGINX after its retirement leaves you and your users vulnerable to attack" (as of September 2026; statement linked in Sources). Detect it with cluster-admin permissions: `kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx`. Any pod returned means migration is required; the `nginx.ingress.kubernetes.io/*` annotations die with the controller. Kubernetes recommends Gateway API over Ingress (whose API is now frozen) and links a migration guide from its Gateway API page. The rest of this guide is the Gateway API form of the old rules. - -## 1. Gateway API with a maintained implementation - -Gateway API is a set of CRDs, not part of core Kubernetes; a controller you install implements them. This guide uses [Envoy Gateway](https://gateway.envoyproxy.io/): `helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.9.1 -n envoy-gateway-system --create-namespace` (version current at the time of writing; the default chart also installs the Gateway API CRDs). The chart does not create a `GatewayClass`; the quickstart applies one separately, and the `Gateway` below refers to it by name, so apply it first and confirm that the controller accepted it: +Gateway API is a set of CRDs, not part of core Kubernetes; a controller you install implements them. This guide uses [Envoy Gateway](https://gateway.envoyproxy.io/): `helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.9.1 -n envoy-gateway-system --create-namespace` (version current at the time of writing; the default chart also installs the Gateway API CRDs). The chart does not create a `GatewayClass`; the quickstart applies one separately, and the `Gateway` below refers to it by name, so apply it first and confirm that the controller accepted it: ```yaml apiVersion: gateway.networking.k8s.io/v1 @@ -719,3 +424,324 @@ allowed ranges out of the provider's own configuration rather than inferring the - AKS API server authorized IP ranges: https://learn.microsoft.com/en-us/azure/aks/api-server-authorized-ip-ranges - cert-manager Gateway API usage (enabling support, annotations; since cert-manager 1.15, Gateway API support no longer requires a feature gate but still requires explicit enablement): https://cert-manager.io/docs/usage/gateway/ ; ACME HTTP-01 `gatewayHTTPRoute` solver: https://cert-manager.io/docs/configuration/acme/http01/ - Traefik Kubernetes Gateway API provider: https://doc.traefik.io/traefik/reference/install-configuration/providers/kubernetes/kubernetes-gateway/ ; Cilium Gateway API support: https://docs.cilium.io/en/stable/network/servicemesh/gateway-api/gateway-api/ + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/kubernetes.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- kubelet: Kubernetes kubelet/kubeadm source (v1.37.1) +- kubernetes: Kubernetes documentation (unknown) +- gateway: Gateway API documentation (unknown) +- envoy: Envoy Gateway (v1.9.1) +- cert-manager: cert-manager Gateway support minimum (1.15) +- apache: Apache htpasswd documentation (unknown) +- authelia: Authelia documentation (unknown) +- eks: Amazon EKS documentation (unknown) +- eks-ami: Amazon EKS AMI bootstrap (6caf8311a3c6a8da71ac7e5e83f9c2e06287039a) +- gke: GKE documentation (unknown) +- gke-min: GKE new-cluster read-only-port boundary (1.32) +- aks: AKS documentation (unknown) +- traefik: Traefik documentation (unknown) +- cilium: Cilium documentation (unknown) +- envoy-rolling: Envoy Gateway documentation (rolling) (unknown) +- curl: curl documentation (unknown) +- nmap: Nmap documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- ingress-migration (REASONED; kubernetes): ingress-nginx retired in March 2026 with no subsequent updates; detect its labeled pods and migrate. Gateway API is recommended and Ingress API is frozen. +- gateway-install (REASONED; envoy, gateway, envoy-rolling): Install Envoy Gateway chart v1.9.1 and its default Gateway CRDs; separately create GatewayClass eg with the documented controllerName and confirm Accepted=True. +- gateway-alternatives (REASONED; traefik, cilium): Traefik uses providers.kubernetesGateway; Cilium uses gatewayAPI.enabled=true and requires kube-proxy replacement. +- gateway-tls (REASONED; gateway, envoy-rolling): Gateway HTTPS listener 443 terminates TLS using app-tls for app.example.com; HTTP 80 is for redirect and ACME challenges. +- gateway-route (REASONED; gateway): HTTPRoute app attaches only to the HTTPS listener via sectionName and forwards to the ClusterIP app Service on port 80. +- gateway-redirect (REASONED; gateway, envoy-rolling): The HTTP route uses RequestRedirect with scheme https and statusCode 301. +- cert-manager-enable (REASONED; cert-manager): From cert-manager 1.15, enable config.gatewayAPI.enabled; install Gateway CRDs before startup or restart cert-manager afterwards. +- cert-manager-issue (REASONED; cert-manager): Gateway issuer annotations produce Certificates per HTTPS-listener Secret using listener hostnames; ACME gatewayHTTPRoute parentRefs selects the challenge Gateway. +- basic-auth-policy (REASONED; gateway, envoy-rolling): Gateway API has no standard auth filter; Envoy SecurityPolicy attaches Basic auth to Gateway/HTTPRoute/GRPCRoute using an htpasswd Secret. +- basic-auth-hash (REASONED; apache, envoy-rolling): Envoy supports SHA hashes only; htpasswd -s is weak SHA-1, so use long random passwords over TLS and keep application login. +- basic-auth-input (REASONED; apache, envoy-rolling): htpasswd -i reads stdin, unlike argv-exposing -b; confirm twice, create with -cis, verify with -vi and only then create the Kubernetes Secret. +- oidc (REASONED; envoy-rolling): Envoy SecurityPolicy oidc uses issuer, clientID, clientSecret and redirectURL; enforce MFA at the identity provider. +- external-auth (REASONED; authelia, envoy-rolling): Authelia is an authorization endpoint, not a traffic proxy; Envoy extAuth.http targets its Service and /api/authz/ext-authz/. +- workload-exposure (REASONED; envoy-rolling, kubernetes): Expose workloads only through the Gateway LoadBalancer; keep databases on ClusterIP without routes and enforce pod access with NetworkPolicies plus database TLS/auth. +- secrets (REASONED; kubernetes): Store credentials in Secrets or an external operator, not committed ConfigMaps or environment literals; kubeconfigs containing credentials also need secret handling. +- eks-endpoint (REASONED; eks): EKS API defaults public; restrict public CIDRs or use private access. Private-only endpoints can resolve publicly to private VPC addresses. +- gke-endpoints (REASONED; gke): GKE authorized networks constrain IP-based access, not its separately IAM-gated DNS endpoint; inspect both and disable unused DNS access. +- aks-endpoint (REASONED; aks): Restrict AKS API access with authorized IP ranges; workload Gateway protection does not secure the control plane. +- kubeconfig (REASONED; kubernetes): Embedded tokens or client keys confer their RBAC access without another factor; exec-plugin configs may be only pointers, but args/env can themselves hold credentials. +- api-port (REASONED; kubernetes, eks): API server commonly uses 6443, but providers may use 443; use the complete kubeconfig endpoint and its actual port. +- etcd-ports (REASONED; kubernetes): etcd client/peer defaults are 2379/2380; require certificates, restrict holders and private access, and retain loopback listeners used by the local API server. +- etcd-storage (REASONED; kubernetes): API resources default to plaintext storage in etcd; valid client certificates are the boundary and etcd access is equivalent to cluster root. +- scheduler-port (REASONED; kubernetes): Scheduler default 10259 must remain restricted to cluster clients. +- controller-port (REASONED; kubernetes): Controller-manager default 10257 must remain restricted to cluster clients. +- proxy-port (REASONED; kubernetes): Worker kube-proxy health default 10256 needs separate node exposure checks. +- nodeports (REASONED; kubernetes): NodePort defaults span 30000 to 32767 TCP/UDP; Service listings do not actively probe that range. +- kubelet-legacy-anonymous (REASONED; kubelet): Legacy kubelet flags default anonymous-auth=true; explicitly disable it where flags control authentication. +- kubelet-legacy-webhook (REASONED; kubelet): Legacy authentication-token-webhook defaults false; explicitly enable it where flags configure authentication. +- kubelet-legacy-authz (REASONED; kubelet, kubernetes): Legacy authorization-mode defaults AlwaysAllow; set Webhook where flags configure authorization. +- kubelet-file-anonymous (REASONED; kubelet): v1beta1 KubeletConfiguration instead defaults authentication.anonymous.enabled=false. +- kubelet-file-webhook (REASONED; kubelet): v1beta1 KubeletConfiguration defaults authentication.webhook.enabled=true. +- kubelet-file-authz (REASONED; kubelet): v1beta1 KubeletConfiguration defaults authorization.mode=Webhook. +- kubelet-precedence (REASONED; kubelet): Explicit flags override --config; --config-dir drop-ins override defaults and the file. Inspect all effective settings together. +- kubeadm (REASONED; kubelet): kubeadm sets anonymous false, token webhook true, Webhook authorization and readOnlyPort 0; user overrides are preserved with warnings. +- eks-bootstrap (REASONED; eks-ami): The pinned EKS AMI bootstrap writes Anonymous.Enabled=false, Mode=Webhook and ReadOnlyPort=0; do not generalize to all managed nodes. +- kubelet-bind (REASONED; kubelet): Secured kubelet defaults to address 0.0.0.0 on 10250; wildcard help includes both families. Bind private addresses and restrict authorized callers. +- kubelet-exec (REASONED; kubelet): 10250 debugging handlers can execute in containers; authentication does not justify public reachability. +- readonly-default (REASONED; kubelet): Legacy read-only port defaults 10255, while v1beta1 readOnlyPort defaults 0; explicitly disable it and remove enabling overrides. +- readonly-auth (REASONED; kubelet): Read-only server uses plain HTTP with no authentication or authorization filter; its caller-supplied bind wiring was outside the inspected source subset. +- readonly-paths (REASONED; kubelet): Read-only handlers expose /pods, /stats/summary, metrics variants and health; v1.37.1 has no /spec handler. Securing 10250 does not secure 10255. +- gke-readonly (REASONED; gke-min): GKE disables the read-only port by default only for new clusters running 1.32+; inspect effective configuration on older or upgraded clusters. +- verify-services (REASONED; kubernetes, envoy-rolling, cert-manager): Only the Gateway should have NodePort/LoadBalancer exposure; verify its public address/DNS and Certificate Ready=True. +- verify-redirect (REASONED; gateway): HTTP should return 301 to the HTTPS application URL. +- verify-basic (REASONED; curl, envoy-rolling): Anonymous Basic-protected access should return 401; require SecurityPolicy Accepted=True and a valid credential reaching the app without the gateway Basic challenge. +- verify-api-target (REASONED; kubernetes): Use the whole kubeconfig endpoint, preserving IPv6 and inferring a schemeless endpoint from both cluster and user TLS settings, not a hardcoded 6443. +- verify-proxies (REASONED; curl): Disable curl config and configured proxies for direct probes; a transparent TLS middlebox can still answer for an unreachable target. +- verify-api-tcp (REASONED; kubernetes, eks): Outside allowed ranges, nc success proves something accepted TCP; corroborate failed probes with provider ranges and multiple vantage points. +- verify-api-http (REASONED; curl, eks): Any HTTP status with curl exit 0 means answered; exit 60 means a TLS peer answered, 7 failed connect, 28 is ambiguous timeout and 6 is DNS failure, not privacy proof. +- verify-node-scan (REASONED; kubernetes, nmap): Scan control-plane hosts first and every public node address in both families; workers alone miss API/etcd and separate etcd hosts are absent from kubectl nodes. +- verify-kubelet (REASONED; kubelet): Untrusted clients should reach neither kubelet port after isolation, while authorized clients still reach 10250; readOnlyPort=0 should leave no 10255 listener even for authorized clients. + +````json +{ + "schema": 1, + "checked": "2026-09-27", + "documentation_checked": "2026-09", + "body_sha256": "1e80b5ac6d9df2701bae1929db0d6cb2f9ce6416a58415b189d509640b8a44c8", + "components": { + "kubelet": { + "name": "Kubernetes kubelet/kubeadm source", + "basis": "v1.37.1", + "sources": { + "sc6056d6413a0": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L196-L224", + "s227d2a259fd3": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L373-L398", + "s568cd52cc674": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/apis/config/v1beta1/defaults.go#L85-L102", + "s2daf96dabbdf": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L281", + "s24ba323c3dab": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L282", + "sd81692a1876f": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L33-L48", + "s0eb30acf2f46": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L95-L100", + "s1c86aaef7307": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L144-L188", + "sb98bd327db92": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/utils.go#L66-L71", + "s3fc405855b4a": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/staging/src/k8s.io/kubelet/config/v1beta1/types.go#L160-L176", + "s98cf98130201": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L181-L250", + "s8bea69523ed9": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L339-L375", + "sa02a895a475c": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/cluster/ports/ports.go#L32-L37", + "s6b753b247f8a": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L477-L572", + "sd00d4cdb9276": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/stats/handler.go#L109-L133", + "s02441dcae3cf": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L577-L610" + } + }, + "kubernetes": { + "name": "Kubernetes documentation", + "basis": "unknown", + "sources": { + "s0f11d6ed39b7": "https://kubernetes.io/blog/2026/01/29/ingress-nginx-statement/", + "sca23283c5a9b": "https://kubernetes.io/docs/concepts/services-networking/gateway/", + "sd8c8df3b9ddc": "https://kubernetes.io/docs/concepts/services-networking/ingress/", + "se41a53ea6778": "https://kubernetes.io/docs/reference/config-api/kubeconfig.v1/", + "sae633285120e": "https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/", + "sd59fb2f35cd7": "https://kubernetes.io/docs/reference/networking/ports-and-protocols/", + "s01f081fed0ff": "https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/", + "s49a29258961b": "https://kubernetes.io/docs/reference/access-authn-authz/kubelet-authn-authz/", + "s04f0c3099fd7": "https://kubernetes.io/docs/reference/kubectl/jsonpath/" + } + }, + "gateway": { + "name": "Gateway API documentation", + "basis": "unknown", + "sources": { + "se0e45c61dc65": "https://gateway-api.sigs.k8s.io/guides/getting-started/introduction/", + "se1f28d98d168": "https://gateway-api.sigs.k8s.io/guides/user-guides/tls/", + "s4a971425e1d1": "https://gateway-api.sigs.k8s.io/guides/user-guides/http-routing/", + "s8d02fbc742ba": "https://gateway-api.sigs.k8s.io/guides/user-guides/http-redirect-rewrite/" + } + }, + "envoy": { + "name": "Envoy Gateway", + "basis": "v1.9.1", + "sources": { + "s6412abbdfdbd": "https://github.com/envoyproxy/gateway/releases/download/v1.9.1/quickstart.yaml" + } + }, + "cert-manager": { + "name": "cert-manager Gateway support minimum", + "basis": "1.15", + "sources": { + "s72bdb97928a8": "https://cert-manager.io/docs/usage/gateway/", + "s8d82244c2d6d": "https://cert-manager.io/docs/configuration/acme/http01/" + } + }, + "apache": { + "name": "Apache htpasswd documentation", + "basis": "unknown", + "sources": { + "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" + } + }, + "authelia": { + "name": "Authelia documentation", + "basis": "unknown", + "sources": { + "s0cb4f49871b5": "https://www.authelia.com/integration/proxies/introduction/", + "scfa9e2cc2ef5": "https://www.authelia.com/integration/kubernetes/envoy/gateway/" + } + }, + "eks": { + "name": "Amazon EKS documentation", + "basis": "unknown", + "sources": { + "s9724ce882930": "https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html" + } + }, + "eks-ami": { + "name": "Amazon EKS AMI bootstrap", + "basis": "6caf8311a3c6a8da71ac7e5e83f9c2e06287039a", + "sources": { + "sbe1fce93c7b2": "https://github.com/awslabs/amazon-eks-ami/blob/6caf8311a3c6a8da71ac7e5e83f9c2e06287039a/nodeadm/internal/kubelet/config.go" + } + }, + "gke": { + "name": "GKE documentation", + "basis": "unknown", + "sources": { + "s0f44d89575ed": "https://docs.cloud.google.com/kubernetes-engine/docs/concepts/network-isolation#how_authorized_networks_work" + } + }, + "gke-min": { + "name": "GKE new-cluster read-only-port boundary", + "basis": "1.32", + "sources": { + "s4c7be4adcab4": "https://docs.cloud.google.com/kubernetes-engine/docs/how-to/disable-kubelet-readonly-port" + } + }, + "aks": { + "name": "AKS documentation", + "basis": "unknown", + "sources": { + "s897dd27ab9a1": "https://learn.microsoft.com/en-us/azure/aks/api-server-authorized-ip-ranges" + } + }, + "traefik": { + "name": "Traefik documentation", + "basis": "unknown", + "sources": { + "s06f2ee45331e": "https://doc.traefik.io/traefik/reference/install-configuration/providers/kubernetes/kubernetes-gateway/" + } + }, + "cilium": { + "name": "Cilium documentation", + "basis": "unknown", + "sources": { + "sacaa7a78461d": "https://docs.cilium.io/en/stable/network/servicemesh/gateway-api/gateway-api/" + } + }, + "envoy-rolling": { + "name": "Envoy Gateway documentation (rolling)", + "basis": "unknown", + "sources": { + "se833317feea7": "https://gateway.envoyproxy.io/docs/tasks/security/secure-gateways/", + "sb5fa4f4ffa1a": "https://gateway.envoyproxy.io/docs/tasks/security/basic-auth/", + "s7fdd76c5d549": "https://gateway.envoyproxy.io/docs/tasks/security/oidc/", + "s100049c74adf": "https://gateway.envoyproxy.io/docs/tasks/security/ext-auth/", + "s9b1d646cb718": "https://gateway.envoyproxy.io/docs/tasks/traffic/http-redirect/", + "s881f08174c54": "https://gateway.envoyproxy.io/v1.9/install/install-helm/", + "s9ad0f418b20b": "https://gateway.envoyproxy.io/v1.9/tasks/quickstart/" + } + }, + "curl": { + "name": "curl documentation", + "basis": "unknown", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html", + "s7d4a6e627b1b": "https://curl.se/libcurl/c/libcurl-errors.html" + } + }, + "nmap": { + "name": "Nmap documentation", + "basis": "unknown", + "sources": { + "sb7d0e7eb5141": "https://nmap.org/book/man-host-discovery.html", + "s1818abeb3f92": "https://nmap.org/book/man-port-specification.html", + "s79c95712dd8d": "https://nmap.org/book/man-misc-options.html" + } + } + }, + "claims": { + "ingress-migration": {"text": "ingress-nginx retired in March 2026 with no subsequent updates; detect its labeled pods and migrate. Gateway API is recommended and Ingress API is frozen.", "components": ["kubernetes"], "sources": ["kubernetes:s0f11d6ed39b7", "kubernetes:sca23283c5a9b", "kubernetes:sd8c8df3b9ddc"], "status": "REASONED"}, + "gateway-install": {"text": "Install Envoy Gateway chart v1.9.1 and its default Gateway CRDs; separately create GatewayClass eg with the documented controllerName and confirm Accepted=True.", "components": ["envoy", "gateway", "envoy-rolling"], "sources": ["envoy-rolling:s881f08174c54", "envoy:s6412abbdfdbd", "gateway:se0e45c61dc65"], "status": "REASONED"}, + "gateway-alternatives": {"text": "Traefik uses providers.kubernetesGateway; Cilium uses gatewayAPI.enabled=true and requires kube-proxy replacement.", "components": ["traefik", "cilium"], "sources": ["traefik:s06f2ee45331e", "cilium:sacaa7a78461d"], "status": "REASONED"}, + "gateway-tls": {"text": "Gateway HTTPS listener 443 terminates TLS using app-tls for app.example.com; HTTP 80 is for redirect and ACME challenges.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:se1f28d98d168", "envoy-rolling:se833317feea7"], "status": "REASONED"}, + "gateway-route": {"text": "HTTPRoute app attaches only to the HTTPS listener via sectionName and forwards to the ClusterIP app Service on port 80.", "components": ["gateway"], "sources": ["gateway:s4a971425e1d1"], "status": "REASONED"}, + "gateway-redirect": {"text": "The HTTP route uses RequestRedirect with scheme https and statusCode 301.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:s8d02fbc742ba", "envoy-rolling:s9b1d646cb718"], "status": "REASONED"}, + "cert-manager-enable": {"text": "From cert-manager 1.15, enable config.gatewayAPI.enabled; install Gateway CRDs before startup or restart cert-manager afterwards.", "components": ["cert-manager"], "sources": ["cert-manager:s72bdb97928a8"], "status": "REASONED"}, + "cert-manager-issue": {"text": "Gateway issuer annotations produce Certificates per HTTPS-listener Secret using listener hostnames; ACME gatewayHTTPRoute parentRefs selects the challenge Gateway.", "components": ["cert-manager"], "sources": ["cert-manager:s72bdb97928a8", "cert-manager:s8d82244c2d6d"], "status": "REASONED"}, + "basic-auth-policy": {"text": "Gateway API has no standard auth filter; Envoy SecurityPolicy attaches Basic auth to Gateway/HTTPRoute/GRPCRoute using an htpasswd Secret.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:s4a971425e1d1", "envoy-rolling:sb5fa4f4ffa1a"], "status": "REASONED"}, + "basic-auth-hash": {"text": "Envoy supports SHA hashes only; htpasswd -s is weak SHA-1, so use long random passwords over TLS and keep application login.", "components": ["apache", "envoy-rolling"], "sources": ["envoy-rolling:sb5fa4f4ffa1a", "apache:scde9004bb967"], "status": "REASONED"}, + "basic-auth-input": {"text": "htpasswd -i reads stdin, unlike argv-exposing -b; confirm twice, create with -cis, verify with -vi and only then create the Kubernetes Secret.", "components": ["apache", "envoy-rolling"], "sources": ["apache:scde9004bb967", "envoy-rolling:sb5fa4f4ffa1a"], "status": "REASONED"}, + "oidc": {"text": "Envoy SecurityPolicy oidc uses issuer, clientID, clientSecret and redirectURL; enforce MFA at the identity provider.", "components": ["envoy-rolling"], "sources": ["envoy-rolling:s7fdd76c5d549"], "status": "REASONED"}, + "external-auth": {"text": "Authelia is an authorization endpoint, not a traffic proxy; Envoy extAuth.http targets its Service and /api/authz/ext-authz/.", "components": ["authelia", "envoy-rolling"], "sources": ["authelia:s0cb4f49871b5", "authelia:scfa9e2cc2ef5", "envoy-rolling:s100049c74adf"], "status": "REASONED"}, + "workload-exposure": {"text": "Expose workloads only through the Gateway LoadBalancer; keep databases on ClusterIP without routes and enforce pod access with NetworkPolicies plus database TLS/auth.", "components": ["envoy-rolling", "kubernetes"], "sources": ["envoy-rolling:s9ad0f418b20b", "kubernetes:sca23283c5a9b"], "status": "REASONED"}, + "secrets": {"text": "Store credentials in Secrets or an external operator, not committed ConfigMaps or environment literals; kubeconfigs containing credentials also need secret handling.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778", "kubernetes:sae633285120e"], "status": "REASONED"}, + "eks-endpoint": {"text": "EKS API defaults public; restrict public CIDRs or use private access. Private-only endpoints can resolve publicly to private VPC addresses.", "components": ["eks"], "sources": ["eks:s9724ce882930"], "status": "REASONED"}, + "gke-endpoints": {"text": "GKE authorized networks constrain IP-based access, not its separately IAM-gated DNS endpoint; inspect both and disable unused DNS access.", "components": ["gke"], "sources": ["gke:s0f44d89575ed"], "status": "REASONED"}, + "aks-endpoint": {"text": "Restrict AKS API access with authorized IP ranges; workload Gateway protection does not secure the control plane.", "components": ["aks"], "sources": ["aks:s897dd27ab9a1"], "status": "REASONED"}, + "kubeconfig": {"text": "Embedded tokens or client keys confer their RBAC access without another factor; exec-plugin configs may be only pointers, but args/env can themselves hold credentials.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778"], "status": "REASONED"}, + "api-port": {"text": "API server commonly uses 6443, but providers may use 443; use the complete kubeconfig endpoint and its actual port.", "components": ["kubernetes", "eks"], "sources": ["kubernetes:sd59fb2f35cd7", "eks:s9724ce882930"], "status": "REASONED"}, + "etcd-ports": {"text": "etcd client/peer defaults are 2379/2380; require certificates, restrict holders and private access, and retain loopback listeners used by the local API server.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7", "kubernetes:s01f081fed0ff"], "status": "REASONED"}, + "etcd-storage": {"text": "API resources default to plaintext storage in etcd; valid client certificates are the boundary and etcd access is equivalent to cluster root.", "components": ["kubernetes"], "sources": ["kubernetes:sae633285120e", "kubernetes:s01f081fed0ff"], "status": "REASONED"}, + "scheduler-port": {"text": "Scheduler default 10259 must remain restricted to cluster clients.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, + "controller-port": {"text": "Controller-manager default 10257 must remain restricted to cluster clients.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, + "proxy-port": {"text": "Worker kube-proxy health default 10256 needs separate node exposure checks.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, + "nodeports": {"text": "NodePort defaults span 30000 to 32767 TCP/UDP; Service listings do not actively probe that range.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, + "kubelet-legacy-anonymous": {"text": "Legacy kubelet flags default anonymous-auth=true; explicitly disable it where flags control authentication.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0", "kubelet:s227d2a259fd3"], "status": "REASONED"}, + "kubelet-legacy-webhook": {"text": "Legacy authentication-token-webhook defaults false; explicitly enable it where flags configure authentication.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0"], "status": "REASONED"}, + "kubelet-legacy-authz": {"text": "Legacy authorization-mode defaults AlwaysAllow; set Webhook where flags configure authorization.", "components": ["kubelet", "kubernetes"], "sources": ["kubelet:sc6056d6413a0", "kubernetes:s49a29258961b"], "status": "REASONED"}, + "kubelet-file-anonymous": {"text": "v1beta1 KubeletConfiguration instead defaults authentication.anonymous.enabled=false.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, + "kubelet-file-webhook": {"text": "v1beta1 KubeletConfiguration defaults authentication.webhook.enabled=true.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, + "kubelet-file-authz": {"text": "v1beta1 KubeletConfiguration defaults authorization.mode=Webhook.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, + "kubelet-precedence": {"text": "Explicit flags override --config; --config-dir drop-ins override defaults and the file. Inspect all effective settings together.", "components": ["kubelet"], "sources": ["kubelet:s2daf96dabbdf", "kubelet:s24ba323c3dab"], "status": "REASONED"}, + "kubeadm": {"text": "kubeadm sets anonymous false, token webhook true, Webhook authorization and readOnlyPort 0; user overrides are preserved with warnings.", "components": ["kubelet"], "sources": ["kubelet:sd81692a1876f", "kubelet:s0eb30acf2f46", "kubelet:s1c86aaef7307", "kubelet:sb98bd327db92"], "status": "REASONED"}, + "eks-bootstrap": {"text": "The pinned EKS AMI bootstrap writes Anonymous.Enabled=false, Mode=Webhook and ReadOnlyPort=0; do not generalize to all managed nodes.", "components": ["eks-ami"], "sources": ["eks-ami:sbe1fce93c7b2"], "status": "REASONED"}, + "kubelet-bind": {"text": "Secured kubelet defaults to address 0.0.0.0 on 10250; wildcard help includes both families. Bind private addresses and restrict authorized callers.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674", "kubelet:s3fc405855b4a", "kubelet:s227d2a259fd3", "kubelet:s98cf98130201"], "status": "REASONED"}, + "kubelet-exec": {"text": "10250 debugging handlers can execute in containers; authentication does not justify public reachability.", "components": ["kubelet"], "sources": ["kubelet:s8bea69523ed9", "kubelet:s3fc405855b4a", "kubelet:s98cf98130201", "kubelet:s02441dcae3cf"], "status": "REASONED"}, + "readonly-default": {"text": "Legacy read-only port defaults 10255, while v1beta1 readOnlyPort defaults 0; explicitly disable it and remove enabling overrides.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0", "kubelet:sa02a895a475c", "kubelet:s3fc405855b4a"], "status": "REASONED"}, + "readonly-auth": {"text": "Read-only server uses plain HTTP with no authentication or authorization filter; its caller-supplied bind wiring was outside the inspected source subset.", "components": ["kubelet"], "sources": ["kubelet:s98cf98130201", "kubelet:s8bea69523ed9"], "status": "REASONED"}, + "readonly-paths": {"text": "Read-only handlers expose /pods, /stats/summary, metrics variants and health; v1.37.1 has no /spec handler. Securing 10250 does not secure 10255.", "components": ["kubelet"], "sources": ["kubelet:s6b753b247f8a", "kubelet:sd00d4cdb9276"], "status": "REASONED"}, + "gke-readonly": {"text": "GKE disables the read-only port by default only for new clusters running 1.32+; inspect effective configuration on older or upgraded clusters.", "components": ["gke-min"], "sources": ["gke-min:s4c7be4adcab4"], "status": "REASONED"}, + "verify-services": {"text": "Only the Gateway should have NodePort/LoadBalancer exposure; verify its public address/DNS and Certificate Ready=True.", "components": ["kubernetes", "envoy-rolling", "cert-manager"], "sources": ["kubernetes:s04f0c3099fd7", "envoy-rolling:s9ad0f418b20b", "cert-manager:s72bdb97928a8"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP should return 301 to the HTTPS application URL.", "components": ["gateway"], "sources": ["gateway:s8d02fbc742ba"], "status": "REASONED", "verify": [1]}, + "verify-basic": {"text": "Anonymous Basic-protected access should return 401; require SecurityPolicy Accepted=True and a valid credential reaching the app without the gateway Basic challenge.", "components": ["curl", "envoy-rolling"], "sources": ["envoy-rolling:sb5fa4f4ffa1a", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, + "verify-api-target": {"text": "Use the whole kubeconfig endpoint, preserving IPv6 and inferring a schemeless endpoint from both cluster and user TLS settings, not a hardcoded 6443.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778", "kubernetes:sd59fb2f35cd7"], "status": "REASONED", "verify": [1]}, + "verify-proxies": {"text": "Disable curl config and configured proxies for direct probes; a transparent TLS middlebox can still answer for an unreachable target.", "components": ["curl"], "sources": ["curl:s2b2686afaf41", "curl:s7d4a6e627b1b"], "status": "REASONED", "verify": [1]}, + "verify-api-tcp": {"text": "Outside allowed ranges, nc success proves something accepted TCP; corroborate failed probes with provider ranges and multiple vantage points.", "components": ["kubernetes", "eks"], "sources": ["kubernetes:sd59fb2f35cd7", "eks:s9724ce882930"], "status": "REASONED", "verify": [1]}, + "verify-api-http": {"text": "Any HTTP status with curl exit 0 means answered; exit 60 means a TLS peer answered, 7 failed connect, 28 is ambiguous timeout and 6 is DNS failure, not privacy proof.", "components": ["curl", "eks"], "sources": ["curl:s7d4a6e627b1b", "eks:s9724ce882930"], "status": "REASONED", "verify": [1]}, + "verify-node-scan": {"text": "Scan control-plane hosts first and every public node address in both families; workers alone miss API/etcd and separate etcd hosts are absent from kubectl nodes.", "components": ["kubernetes", "nmap"], "sources": ["kubernetes:sd59fb2f35cd7", "kubernetes:s01f081fed0ff", "nmap:sb7d0e7eb5141", "nmap:s1818abeb3f92", "nmap:s79c95712dd8d"], "status": "REASONED", "verify": [1]}, + "verify-kubelet": {"text": "Untrusted clients should reach neither kubelet port after isolation, while authorized clients still reach 10250; readOnlyPort=0 should leave no 10255 listener even for authorized clients.", "components": ["kubelet"], "sources": ["kubelet:s98cf98130201", "kubelet:s3fc405855b4a"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/lighttpd.md b/lighttpd.md index 6c885769e..08de50591 100644 --- a/lighttpd.md +++ b/lighttpd.md @@ -1,213 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-19", - "documentation_checked": "2026-09", - "body_sha256": "aa010c3e401699d0b9ff9541720750e57529d097494238fca31e5db6a0c3826f", - "components": { - "tls": { - "name": "lighttpd TLS minimum", - "basis": "1.4.56", - "sources": { - "sbff3cb2e01a8": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_SSL" - } - }, - "key": { - "name": "lighttpd separate-key minimum", - "basis": "1.4.53", - "sources": { - "sbff3cb2e01a8": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_SSL" - } - }, - "redirect": { - "name": "lighttpd redirect default transition", - "basis": "1.4.75", - "sources": { - "s6de629323c64": "https://redmine.lighttpd.net/projects/lighttpd/wiki/HowToRedirectHttpToHttps" - } - }, - "docs": { - "name": "lighttpd documentation", - "basis": "unknown", - "sources": { - "s1dbd1ce27da5": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Mod_auth", - "s6d5666b862a0": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ConfigurationOptions", - "s6de629323c64": "https://redmine.lighttpd.net/projects/lighttpd/wiki/HowToRedirectHttpToHttps" - } - }, - "modules-source": { - "name": "lighttpd module loader source", - "basis": "1.4.75", - "sources": { - "s8680e561bed2": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L395-L544", - "s8e9688979427": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L270-L290", - "s9bd716d27e8d": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L382-L393", - "s21288573041a": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L662-L945", - "s67bb3733d81c": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L1684-L1711" - } - }, - "authn-source": { - "name": "lighttpd file authentication source", - "basis": "1.4.75", - "sources": { - "s2c11c0fbc0ca": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_authn_file.c#L340-L397", - "se971effb489f": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_authn_file.c#L613-L740" - } - }, - "htpasswd-tool": { - "name": "Apache htpasswd manual", - "basis": "2.4", - "sources": { - "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" - } - }, - "debian-package": { - "name": "Debian apache2-utils package", - "basis": "trixie", - "sources": { - "s436bdf5f1fb9": "https://packages.debian.org/trixie/apache2-utils" - } - }, - "authelia": { - "name": "Authelia proxy support", - "basis": "v4.39.28", - "sources": { - "s77083bc8faa2": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L24-L36" - } - }, - "cloudflare-access": { - "name": "Cloudflare Access documentation", - "basis": "commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104", - "sources": { - "sfddb92e438ca": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L14-L16", - "s7cafe1b17afd": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54", - "s603bdb6b24ab": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L45-L45", - "s863803bd9a1f": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L48-L48", - "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" - } - }, - "fedora-package": { - "name": "Fedora httpd-tools package", - "basis": "unknown", - "sources": { - "s919088d03a5e": "https://packages.fedoraproject.org/pkgs/httpd/httpd-tools/" - } - }, - "lighttpd-verify-source": { - "name": "lighttpd verify source", - "basis": "1.4.75", - "sources": { - "s0bbb4c861d5e": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/lighttpd.8#L22-L23", - "sca076bcd18dc": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/lighttpd.8#L33-L37", - "s9e69235fbbc7": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1439-L1451", - "s87104d328493": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1466-L1467", - "sba72bab46867": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1510-L1523", - "s2faa9b3149ac": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1942-L1953", - "s87b4b266bb0e": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L2320-L2384", - "sa989ef425f1d": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L229-L235", - "sf9d56b052c61": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/systemd/lighttpd.service#L5-L11", - "s6a29b290d83a": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/request.c#L423-L430", - "s2fc4cf82abd9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/request.c#L1235-L1247", - "s3f692a48bd01": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile-glue.c#L504-L508", - "sbb09703388c3": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L213-L223", - "s8bb6bd7774f9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_openssl.c#L1025-L1034", - "sb4c43fa1a807": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_openssl.c#L1294-L1330", - "s9e49b928cc81": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/response.c#L227-L228" - } - }, - "systemd-reload": { - "name": "systemd reload manual", - "basis": "v262", - "sources": { - "sb3f5069cd250": "https://github.com/systemd/systemd/blob/v262/man/systemctl.xml#L462-L474", - "s441757b7048f": "https://github.com/systemd/systemd/blob/v262/man/systemd.service.xml#L498-L519" - } - }, - "apache-vhosts": { - "name": "Apache virtual-host source", - "basis": "2.4.69", - "sources": { - "s558db45d4d61": "https://github.com/apache/httpd/blob/2.4.69/docs/manual/vhosts/details.xml#L217-L231" - } - }, - "release-news": { - "name": "lighttpd release NEWS", - "basis": "1.4.75", - "sources": { - "s4e990a9e5ec1": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1660-L1660", - "se97c93bf64a8": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1927-L1927", - "s1658d22c71f9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L2465-L2465", - "s95dd525fe4ac": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L2505-L2505", - "sc0d0162991d5": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L6-L7" - } - } - }, - "claims": { - "tls-defaults": {"text": "For this guide's 1.4.56-and-later scope, NEWS records the mod_openssl MinProtocol TLSv1.2 default in 1.4.56. Disabling SSLv2, SSLv3, TLS 1.0 and TLS 1.1 is this guide's reading of that floor; NEWS does not establish defaults for every later release.", "components": ["tls", "release-news"], "sources": ["tls:sbff3cb2e01a8", "release-news:s4e990a9e5ec1", "release-news:se97c93bf64a8"], "status": "REASONED"}, - "tls-listeners": {"text": "Load mod_openssl and enable TLS on both :443 and [::]:443 with ssl.pemfile and ssl.privkey; omitting the IPv6 socket leaves IPv6 clients on plain HTTP.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED"}, - "certificate-chain": {"text": "ssl.pemfile uses fullchain.pem rather than a certificate without its chain.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED"}, - "separate-key": {"text": "NEWS records optional mod_openssl ssl.privkey in 1.4.53. On older versions, concatenate certificate and key into ssl.pemfile (this guide's compatibility advice; the entry does not establish that procedure).", "components": ["key", "release-news"], "sources": ["key:sbff3cb2e01a8", "release-news:s1658d22c71f9", "release-news:s95dd525fe4ac"], "status": "REASONED"}, - "protocol-floor": {"text": "Set MinProtocol to TLSv1.2 with ssl.openssl.ssl-conf-cmd explicitly (this guide's advice); NEWS records that default for mod_openssl in 1.4.56, not every later release.", "components": ["tls", "release-news"], "sources": ["tls:sbff3cb2e01a8", "release-news:s4e990a9e5ec1", "release-news:se97c93bf64a8"], "status": "REASONED"}, - "module-defaults": {"text": "In lighttpd 1.4.75, mod_h2 is inserted unless server.feature-flags disables server.h2proto (enabled by default); when server.compat-module-load is enabled (the default), mod_indexfile and mod_dirlisting are conditionally added when enabled, mod_staticfile is added when absent, and compatibility handling can also add other modules, including authentication and TLS modules under their respective conditions. Load mod_redirect explicitly.", "components": ["docs", "modules-source"], "sources": ["docs:s6d5666b862a0", "modules-source:s8680e561bed2", "modules-source:s8e9688979427", "modules-source:s9bd716d27e8d", "modules-source:s21288573041a", "modules-source:s67bb3733d81c"], "status": "REASONED"}, - "http-redirect": {"text": "For the http scheme, redirect to HTTPS preserving authority, path and query; port 80 must redirect rather than serve content.", "components": ["docs"], "sources": ["docs:s6de629323c64"], "status": "REASONED"}, - "redirect-code": {"text": "NEWS records url.redirect-code = 308 as the new default in 1.4.75. Set 308 explicitly before 1.4.75 (this guide's advice; the entry does not specify each earlier default).", "components": ["redirect", "release-news"], "sources": ["redirect:s6de629323c64", "release-news:sc0d0162991d5"], "status": "REASONED"}, - "basic-auth": {"text": "Load mod_auth and mod_authn_file, select the htpasswd file backend and require valid-user Basic authentication on / over TLS; application login is preferable.", "components": ["docs"], "sources": ["docs:s1dbd1ce27da5"], "status": "REASONED"}, - "password-file": {"text": "Create the user file with Apache htpasswd (Debian apache2-utils; Fedora httpd-tools is this guide's package assessment); lighttpd 1.4.75 reads username:stored-hash entries and checks {SHA} SHA-1 and $apr1$ internally, with other hashes of at least 13 characters checked through crypt() when built with crypt support; under USE_LIB_CRYPTO_MD4, the $1+ntlm$ path derives an NTLM hash and calls crypt() with that derived value and a $1$ salt. Check the build and platform's accepted hashes before choosing a flag; this check is this guide's compatibility advice.", "components": ["docs", "authn-source", "htpasswd-tool", "debian-package", "fedora-package"], "sources": ["docs:s1dbd1ce27da5", "authn-source:s2c11c0fbc0ca", "authn-source:se971effb489f", "htpasswd-tool:scde9004bb967", "debian-package:s436bdf5f1fb9", "fedora-package:s919088d03a5e"], "status": "REASONED"}, - "mfa": {"text": "Basic authentication is single-factor; add MFA by fronting this service with Cloudflare Access or an MFA-capable proxy (this guide's requirement). Authelia v4.39.28's complete supported-proxy list has no lighttpd entry (this guide's inference from the table); Cloudflare Access documents self-hosted application and MFA policy controls; applying them to lighttpd is this guide's assessment. This guide assesses that a lighttpd origin reachable without passing through Access is not protected by it.", "components": ["docs", "authelia", "cloudflare-access"], "sources": ["docs:s1dbd1ce27da5", "authelia:s77083bc8faa2", "cloudflare-access:sfddb92e438ca", "cloudflare-access:s7cafe1b17afd", "cloudflare-access:sdcac732541da", "cloudflare-access:s603bdb6b24ab", "cloudflare-access:s863803bd9a1f"], "status": "REASONED"}, - "verify-config": {"text": "lighttpd 1.4.75 -tt -f /etc/lighttpd/lighttpd.conf tests the named file for syntax errors and initializes modules; configuration failures prevent the && reload (this guide's inference from the nonzero process result and shell chaining). The cited upstream unit starts from that file and defines a USR1 reload through ExecReload; systemctl reload triggers the service action, whose signal does not establish completion. Confirm the installed unit uses that file and defines reload (this guide's advice).", "components": ["docs", "lighttpd-verify-source", "systemd-reload"], "sources": ["docs:s6d5666b862a0", "lighttpd-verify-source:s0bbb4c861d5e", "lighttpd-verify-source:sca076bcd18dc", "lighttpd-verify-source:s9e69235fbbc7", "lighttpd-verify-source:s87104d328493", "lighttpd-verify-source:sba72bab46867", "lighttpd-verify-source:s2faa9b3149ac", "lighttpd-verify-source:s87b4b266bb0e", "lighttpd-verify-source:sa989ef425f1d", "lighttpd-verify-source:sf9d56b052c61", "systemd-reload:sb3f5069cd250", "systemd-reload:s441757b7048f"], "status": "REASONED", "verify": [1]}, - "verify-redirect": {"text": "The HTTP header request should show a redirect to HTTPS; these are read-and-judge status checks.", "components": ["docs"], "sources": ["docs:s6de629323c64"], "status": "REASONED", "verify": [1]}, - "verify-auth": {"text": "An HTTPS request without credentials should return 401 once authentication is enabled.", "components": ["docs"], "sources": ["docs:s1dbd1ce27da5"], "status": "REASONED", "verify": [1]}, - "verify-listeners": {"text": "Read every listener with ss -tlnp: front-door authentication does not establish that no other listener serves content. The ss version is unrecorded.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED", "verify": [1]}, - "verify-host": {"text": "In lighttpd 1.4.75, request-time $HTTP[\"host\"] matching uses the HTTP Host value, while TLS SNI can select mod_openssl settings before request headers; a Host outside a conditional auth.require falls back to global configuration for that rule (this guide's inference from conditional merging and mod_auth documentation). On a protected path, this guide interprets 401 as the pass and 200 as the protected path served without authentication on this TLS connection to a client that sends another host. Apache 2.4.69 uses the SNI hostname, when the client sends it and the server supports SNI, for name-based TLS virtual-host selection; varying Host while keeping URL and SNI fixed is this guide's auth-check advice.", "components": ["docs", "lighttpd-verify-source", "apache-vhosts"], "sources": ["docs:s1dbd1ce27da5", "lighttpd-verify-source:s6a29b290d83a", "lighttpd-verify-source:s2fc4cf82abd9", "lighttpd-verify-source:s3f692a48bd01", "lighttpd-verify-source:sbb09703388c3", "lighttpd-verify-source:s8bb6bd7774f9", "lighttpd-verify-source:sb4c43fa1a807", "lighttpd-verify-source:s9e49b928cc81", "apache-vhosts:s558db45d4d61"], "status": "REASONED", "verify": [1]} - } -} ---- # lighttpd: TLS and authentication - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +Written against lighttpd TLS minimum 1.4.56, lighttpd separate-key minimum 1.4.53, lighttpd redirect default transition 1.4.75, lighttpd module loader source 1.4.75, lighttpd file authentication source 1.4.75, lighttpd verify source 1.4.75, Apache htpasswd manual 2.4, and Debian apache2-utils package trixie, plus 5 more recorded bases; 2 further sources carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 16 claims, all reasoned from cited sources. -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- tls: lighttpd TLS minimum (1.4.56) -- key: lighttpd separate-key minimum (1.4.53) -- redirect: lighttpd redirect default transition (1.4.75) -- docs: lighttpd documentation (unknown) -- modules-source: lighttpd module loader source (1.4.75) -- authn-source: lighttpd file authentication source (1.4.75) -- htpasswd-tool: Apache htpasswd manual (2.4) -- debian-package: Debian apache2-utils package (trixie) -- authelia: Authelia proxy support (v4.39.28) -- cloudflare-access: Cloudflare Access documentation (commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- fedora-package: Fedora httpd-tools package (unknown) -- lighttpd-verify-source: lighttpd verify source (1.4.75) -- systemd-reload: systemd reload manual (v262) -- apache-vhosts: Apache virtual-host source (2.4.69) -- release-news: lighttpd release NEWS (1.4.75) - -Claims (full source mapping in the front matter): - -- tls-defaults (REASONED; tls, release-news): For this guide's 1.4.56-and-later scope, NEWS records the mod_openssl MinProtocol TLSv1.2 default in 1.4.56. Disabling SSLv2, SSLv3, TLS 1.0 and TLS 1.1 is this guide's reading of that floor; NEWS does not establish defaults for every later release. -- tls-listeners (REASONED; tls): Load mod_openssl and enable TLS on both :443 and [::]:443 with ssl.pemfile and ssl.privkey; omitting the IPv6 socket leaves IPv6 clients on plain HTTP. -- certificate-chain (REASONED; tls): ssl.pemfile uses fullchain.pem rather than a certificate without its chain. -- separate-key (REASONED; key, release-news): NEWS records optional mod_openssl ssl.privkey in 1.4.53. On older versions, concatenate certificate and key into ssl.pemfile (this guide's compatibility advice; the entry does not establish that procedure). -- protocol-floor (REASONED; tls, release-news): Set MinProtocol to TLSv1.2 with ssl.openssl.ssl-conf-cmd explicitly (this guide's advice); NEWS records that default for mod_openssl in 1.4.56, not every later release. -- module-defaults (REASONED; docs, modules-source): In lighttpd 1.4.75, mod_h2 is inserted unless server.feature-flags disables server.h2proto (enabled by default); when server.compat-module-load is enabled (the default), mod_indexfile and mod_dirlisting are conditionally added when enabled, mod_staticfile is added when absent, and compatibility handling can also add other modules, including authentication and TLS modules under their respective conditions. Load mod_redirect explicitly. -- http-redirect (REASONED; docs): For the http scheme, redirect to HTTPS preserving authority, path and query; port 80 must redirect rather than serve content. -- redirect-code (REASONED; redirect, release-news): NEWS records url.redirect-code = 308 as the new default in 1.4.75. Set 308 explicitly before 1.4.75 (this guide's advice; the entry does not specify each earlier default). -- basic-auth (REASONED; docs): Load mod_auth and mod_authn_file, select the htpasswd file backend and require valid-user Basic authentication on / over TLS; application login is preferable. -- password-file (REASONED; docs, authn-source, htpasswd-tool, debian-package, fedora-package): Create the user file with Apache htpasswd (Debian apache2-utils; Fedora httpd-tools is this guide's package assessment); lighttpd 1.4.75 reads username:stored-hash entries and checks {SHA} SHA-1 and $apr1$ internally, with other hashes of at least 13 characters checked through crypt() when built with crypt support; under USE_LIB_CRYPTO_MD4, the $1+ntlm$ path derives an NTLM hash and calls crypt() with that derived value and a $1$ salt. Check the build and platform's accepted hashes before choosing a flag; this check is this guide's compatibility advice. -- mfa (REASONED; docs, authelia, cloudflare-access): Basic authentication is single-factor; add MFA by fronting this service with Cloudflare Access or an MFA-capable proxy (this guide's requirement). Authelia v4.39.28's complete supported-proxy list has no lighttpd entry (this guide's inference from the table); Cloudflare Access documents self-hosted application and MFA policy controls; applying them to lighttpd is this guide's assessment. This guide assesses that a lighttpd origin reachable without passing through Access is not protected by it. -- verify-config (REASONED; docs, lighttpd-verify-source, systemd-reload): lighttpd 1.4.75 -tt -f /etc/lighttpd/lighttpd.conf tests the named file for syntax errors and initializes modules; configuration failures prevent the && reload (this guide's inference from the nonzero process result and shell chaining). The cited upstream unit starts from that file and defines a USR1 reload through ExecReload; systemctl reload triggers the service action, whose signal does not establish completion. Confirm the installed unit uses that file and defines reload (this guide's advice). -- verify-redirect (REASONED; docs): The HTTP header request should show a redirect to HTTPS; these are read-and-judge status checks. -- verify-auth (REASONED; docs): An HTTPS request without credentials should return 401 once authentication is enabled. -- verify-listeners (REASONED; tls): Read every listener with ss -tlnp: front-door authentication does not establish that no other listener serves content. The ss version is unrecorded. -- verify-host (REASONED; docs, lighttpd-verify-source, apache-vhosts): In lighttpd 1.4.75, request-time $HTTP["host"] matching uses the HTTP Host value, while TLS SNI can select mod_openssl settings before request headers; a Host outside a conditional auth.require falls back to global configuration for that rule (this guide's inference from conditional merging and mod_auth documentation). On a protected path, this guide interprets 401 as the pass and 200 as the protected path served without authentication on this TLS connection to a client that sends another host. Apache 2.4.69 uses the SNI hostname, when the client sends it and the server supports SNI, for name-based TLS virtual-host selection; varying Host while keeping URL and SNI fixed is this guide's auth-check advice. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Applies to lighttpd 1.4.56 and later. NEWS records the `mod_openssl` `MinProtocol TLSv1.2` default in its 1.4.56 release section (https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1660-L1660; https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1927-L1927). Disabling SSLv2/SSLv3/TLS 1.0/TLS 1.1 is this guide's reading of that floor; NEWS does not establish defaults for every later release. Get a certificate first: [free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md). @@ -361,3 +156,234 @@ curl -q -s -o /dev/null -w '%{http_code}\n' -H 'Host: not-configured.example' \ - Cloudflare Access origin reachability at commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L45-L45 ; https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L48-L48 - Cloudflare Access MFA policy controls at commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81 - Fedora `httpd-tools` package (checked October 2026): https://packages.fedoraproject.org/pkgs/httpd/httpd-tools/ + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/lighttpd.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- tls: lighttpd TLS minimum (1.4.56) +- key: lighttpd separate-key minimum (1.4.53) +- redirect: lighttpd redirect default transition (1.4.75) +- modules-source: lighttpd module loader source (1.4.75) +- authn-source: lighttpd file authentication source (1.4.75) +- lighttpd-verify-source: lighttpd verify source (1.4.75) +- docs: lighttpd documentation (unknown) +- htpasswd-tool: Apache htpasswd manual (2.4) +- debian-package: Debian apache2-utils package (trixie) +- authelia: Authelia proxy support (v4.39.28) +- cloudflare-access: Cloudflare Access documentation (commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- fedora-package: Fedora httpd-tools package (unknown) +- apache-vhosts: Apache virtual-host source (2.4.69) +- systemd-reload: systemd reload manual (v262) +- release-news: lighttpd release NEWS (1.4.75) + +Claims (full source mapping in the JSON block below): + +- tls-defaults (REASONED; tls, release-news): For this guide's 1.4.56-and-later scope, NEWS records the mod_openssl MinProtocol TLSv1.2 default in 1.4.56. Disabling SSLv2, SSLv3, TLS 1.0 and TLS 1.1 is this guide's reading of that floor; NEWS does not establish defaults for every later release. +- tls-listeners (REASONED; tls): Load mod_openssl and enable TLS on both :443 and [::]:443 with ssl.pemfile and ssl.privkey; omitting the IPv6 socket leaves IPv6 clients on plain HTTP. +- certificate-chain (REASONED; tls): ssl.pemfile uses fullchain.pem rather than a certificate without its chain. +- separate-key (REASONED; key, release-news): NEWS records optional mod_openssl ssl.privkey in 1.4.53. On older versions, concatenate certificate and key into ssl.pemfile (this guide's compatibility advice; the entry does not establish that procedure). +- protocol-floor (REASONED; tls, release-news): Set MinProtocol to TLSv1.2 with ssl.openssl.ssl-conf-cmd explicitly (this guide's advice); NEWS records that default for mod_openssl in 1.4.56, not every later release. +- module-defaults (REASONED; docs, modules-source): In lighttpd 1.4.75, mod_h2 is inserted unless server.feature-flags disables server.h2proto (enabled by default); when server.compat-module-load is enabled (the default), mod_indexfile and mod_dirlisting are conditionally added when enabled, mod_staticfile is added when absent, and compatibility handling can also add other modules, including authentication and TLS modules under their respective conditions. Load mod_redirect explicitly. +- http-redirect (REASONED; docs): For the http scheme, redirect to HTTPS preserving authority, path and query; port 80 must redirect rather than serve content. +- redirect-code (REASONED; redirect, release-news): NEWS records url.redirect-code = 308 as the new default in 1.4.75. Set 308 explicitly before 1.4.75 (this guide's advice; the entry does not specify each earlier default). +- basic-auth (REASONED; docs): Load mod_auth and mod_authn_file, select the htpasswd file backend and require valid-user Basic authentication on / over TLS; application login is preferable. +- password-file (REASONED; docs, authn-source, htpasswd-tool, debian-package, fedora-package): Create the user file with Apache htpasswd (Debian apache2-utils; Fedora httpd-tools is this guide's package assessment); lighttpd 1.4.75 reads username:stored-hash entries and checks {SHA} SHA-1 and $apr1$ internally, with other hashes of at least 13 characters checked through crypt() when built with crypt support; under USE_LIB_CRYPTO_MD4, the $1+ntlm$ path derives an NTLM hash and calls crypt() with that derived value and a $1$ salt. Check the build and platform's accepted hashes before choosing a flag; this check is this guide's compatibility advice. +- mfa (REASONED; docs, authelia, cloudflare-access): Basic authentication is single-factor; add MFA by fronting this service with Cloudflare Access or an MFA-capable proxy (this guide's requirement). Authelia v4.39.28's complete supported-proxy list has no lighttpd entry (this guide's inference from the table); Cloudflare Access documents self-hosted application and MFA policy controls; applying them to lighttpd is this guide's assessment. This guide assesses that a lighttpd origin reachable without passing through Access is not protected by it. +- verify-config (REASONED; docs, lighttpd-verify-source, systemd-reload): lighttpd 1.4.75 -tt -f /etc/lighttpd/lighttpd.conf tests the named file for syntax errors and initializes modules; configuration failures prevent the && reload (this guide's inference from the nonzero process result and shell chaining). The cited upstream unit starts from that file and defines a USR1 reload through ExecReload; systemctl reload triggers the service action, whose signal does not establish completion. Confirm the installed unit uses that file and defines reload (this guide's advice). +- verify-redirect (REASONED; docs): The HTTP header request should show a redirect to HTTPS; these are read-and-judge status checks. +- verify-auth (REASONED; docs): An HTTPS request without credentials should return 401 once authentication is enabled. +- verify-listeners (REASONED; tls): Read every listener with ss -tlnp: front-door authentication does not establish that no other listener serves content. The ss version is unrecorded. +- verify-host (REASONED; docs, lighttpd-verify-source, apache-vhosts): In lighttpd 1.4.75, request-time $HTTP["host"] matching uses the HTTP Host value, while TLS SNI can select mod_openssl settings before request headers; a Host outside a conditional auth.require falls back to global configuration for that rule (this guide's inference from conditional merging and mod_auth documentation). On a protected path, this guide interprets 401 as the pass and 200 as the protected path served without authentication on this TLS connection to a client that sends another host. Apache 2.4.69 uses the SNI hostname, when the client sends it and the server supports SNI, for name-based TLS virtual-host selection; varying Host while keeping URL and SNI fixed is this guide's auth-check advice. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "aa010c3e401699d0b9ff9541720750e57529d097494238fca31e5db6a0c3826f", + "components": { + "tls": { + "name": "lighttpd TLS minimum", + "basis": "1.4.56", + "sources": { + "sbff3cb2e01a8": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_SSL" + } + }, + "key": { + "name": "lighttpd separate-key minimum", + "basis": "1.4.53", + "sources": { + "sbff3cb2e01a8": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_SSL" + } + }, + "redirect": { + "name": "lighttpd redirect default transition", + "basis": "1.4.75", + "sources": { + "s6de629323c64": "https://redmine.lighttpd.net/projects/lighttpd/wiki/HowToRedirectHttpToHttps" + } + }, + "modules-source": { + "name": "lighttpd module loader source", + "basis": "1.4.75", + "sources": { + "s8680e561bed2": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L395-L544", + "s8e9688979427": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L270-L290", + "s9bd716d27e8d": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L382-L393", + "s21288573041a": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L662-L945", + "s67bb3733d81c": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L1684-L1711" + } + }, + "authn-source": { + "name": "lighttpd file authentication source", + "basis": "1.4.75", + "sources": { + "s2c11c0fbc0ca": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_authn_file.c#L340-L397", + "se971effb489f": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_authn_file.c#L613-L740" + } + }, + "lighttpd-verify-source": { + "name": "lighttpd verify source", + "basis": "1.4.75", + "sources": { + "s0bbb4c861d5e": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/lighttpd.8#L22-L23", + "sca076bcd18dc": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/lighttpd.8#L33-L37", + "s9e69235fbbc7": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1439-L1451", + "s87104d328493": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1466-L1467", + "sba72bab46867": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1510-L1523", + "s2faa9b3149ac": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1942-L1953", + "s87b4b266bb0e": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L2320-L2384", + "sa989ef425f1d": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L229-L235", + "sf9d56b052c61": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/systemd/lighttpd.service#L5-L11", + "s6a29b290d83a": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/request.c#L423-L430", + "s2fc4cf82abd9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/request.c#L1235-L1247", + "s3f692a48bd01": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile-glue.c#L504-L508", + "sbb09703388c3": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L213-L223", + "s8bb6bd7774f9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_openssl.c#L1025-L1034", + "sb4c43fa1a807": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_openssl.c#L1294-L1330", + "s9e49b928cc81": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/response.c#L227-L228" + } + }, + "docs": { + "name": "lighttpd documentation", + "basis": "unknown", + "sources": { + "s1dbd1ce27da5": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Mod_auth", + "s6d5666b862a0": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ConfigurationOptions", + "s6de629323c64": "https://redmine.lighttpd.net/projects/lighttpd/wiki/HowToRedirectHttpToHttps" + } + }, + "htpasswd-tool": { + "name": "Apache htpasswd manual", + "basis": "2.4", + "sources": { + "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" + } + }, + "debian-package": { + "name": "Debian apache2-utils package", + "basis": "trixie", + "sources": { + "s436bdf5f1fb9": "https://packages.debian.org/trixie/apache2-utils" + } + }, + "authelia": { + "name": "Authelia proxy support", + "basis": "v4.39.28", + "sources": { + "s77083bc8faa2": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L24-L36" + } + }, + "cloudflare-access": { + "name": "Cloudflare Access documentation", + "basis": "commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "sfddb92e438ca": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L14-L16", + "s7cafe1b17afd": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54", + "s603bdb6b24ab": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L45-L45", + "s863803bd9a1f": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L48-L48", + "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" + } + }, + "fedora-package": { + "name": "Fedora httpd-tools package", + "basis": "unknown", + "sources": { + "s919088d03a5e": "https://packages.fedoraproject.org/pkgs/httpd/httpd-tools/" + } + }, + "apache-vhosts": { + "name": "Apache virtual-host source", + "basis": "2.4.69", + "sources": { + "s558db45d4d61": "https://github.com/apache/httpd/blob/2.4.69/docs/manual/vhosts/details.xml#L217-L231" + } + }, + "systemd-reload": { + "name": "systemd reload manual", + "basis": "v262", + "sources": { + "sb3f5069cd250": "https://github.com/systemd/systemd/blob/v262/man/systemctl.xml#L462-L474", + "s441757b7048f": "https://github.com/systemd/systemd/blob/v262/man/systemd.service.xml#L498-L519" + } + }, + "release-news": { + "name": "lighttpd release NEWS", + "basis": "1.4.75", + "sources": { + "s4e990a9e5ec1": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1660-L1660", + "se97c93bf64a8": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1927-L1927", + "s1658d22c71f9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L2465-L2465", + "s95dd525fe4ac": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L2505-L2505", + "sc0d0162991d5": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L6-L7" + } + } + }, + "claims": { + "tls-defaults": {"text": "For this guide's 1.4.56-and-later scope, NEWS records the mod_openssl MinProtocol TLSv1.2 default in 1.4.56. Disabling SSLv2, SSLv3, TLS 1.0 and TLS 1.1 is this guide's reading of that floor; NEWS does not establish defaults for every later release.", "components": ["tls", "release-news"], "sources": ["tls:sbff3cb2e01a8", "release-news:s4e990a9e5ec1", "release-news:se97c93bf64a8"], "status": "REASONED"}, + "tls-listeners": {"text": "Load mod_openssl and enable TLS on both :443 and [::]:443 with ssl.pemfile and ssl.privkey; omitting the IPv6 socket leaves IPv6 clients on plain HTTP.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED"}, + "certificate-chain": {"text": "ssl.pemfile uses fullchain.pem rather than a certificate without its chain.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED"}, + "separate-key": {"text": "NEWS records optional mod_openssl ssl.privkey in 1.4.53. On older versions, concatenate certificate and key into ssl.pemfile (this guide's compatibility advice; the entry does not establish that procedure).", "components": ["key", "release-news"], "sources": ["key:sbff3cb2e01a8", "release-news:s1658d22c71f9", "release-news:s95dd525fe4ac"], "status": "REASONED"}, + "protocol-floor": {"text": "Set MinProtocol to TLSv1.2 with ssl.openssl.ssl-conf-cmd explicitly (this guide's advice); NEWS records that default for mod_openssl in 1.4.56, not every later release.", "components": ["tls", "release-news"], "sources": ["tls:sbff3cb2e01a8", "release-news:s4e990a9e5ec1", "release-news:se97c93bf64a8"], "status": "REASONED"}, + "module-defaults": {"text": "In lighttpd 1.4.75, mod_h2 is inserted unless server.feature-flags disables server.h2proto (enabled by default); when server.compat-module-load is enabled (the default), mod_indexfile and mod_dirlisting are conditionally added when enabled, mod_staticfile is added when absent, and compatibility handling can also add other modules, including authentication and TLS modules under their respective conditions. Load mod_redirect explicitly.", "components": ["docs", "modules-source"], "sources": ["docs:s6d5666b862a0", "modules-source:s8680e561bed2", "modules-source:s8e9688979427", "modules-source:s9bd716d27e8d", "modules-source:s21288573041a", "modules-source:s67bb3733d81c"], "status": "REASONED"}, + "http-redirect": {"text": "For the http scheme, redirect to HTTPS preserving authority, path and query; port 80 must redirect rather than serve content.", "components": ["docs"], "sources": ["docs:s6de629323c64"], "status": "REASONED"}, + "redirect-code": {"text": "NEWS records url.redirect-code = 308 as the new default in 1.4.75. Set 308 explicitly before 1.4.75 (this guide's advice; the entry does not specify each earlier default).", "components": ["redirect", "release-news"], "sources": ["redirect:s6de629323c64", "release-news:sc0d0162991d5"], "status": "REASONED"}, + "basic-auth": {"text": "Load mod_auth and mod_authn_file, select the htpasswd file backend and require valid-user Basic authentication on / over TLS; application login is preferable.", "components": ["docs"], "sources": ["docs:s1dbd1ce27da5"], "status": "REASONED"}, + "password-file": {"text": "Create the user file with Apache htpasswd (Debian apache2-utils; Fedora httpd-tools is this guide's package assessment); lighttpd 1.4.75 reads username:stored-hash entries and checks {SHA} SHA-1 and $apr1$ internally, with other hashes of at least 13 characters checked through crypt() when built with crypt support; under USE_LIB_CRYPTO_MD4, the $1+ntlm$ path derives an NTLM hash and calls crypt() with that derived value and a $1$ salt. Check the build and platform's accepted hashes before choosing a flag; this check is this guide's compatibility advice.", "components": ["docs", "authn-source", "htpasswd-tool", "debian-package", "fedora-package"], "sources": ["docs:s1dbd1ce27da5", "authn-source:s2c11c0fbc0ca", "authn-source:se971effb489f", "htpasswd-tool:scde9004bb967", "debian-package:s436bdf5f1fb9", "fedora-package:s919088d03a5e"], "status": "REASONED"}, + "mfa": {"text": "Basic authentication is single-factor; add MFA by fronting this service with Cloudflare Access or an MFA-capable proxy (this guide's requirement). Authelia v4.39.28's complete supported-proxy list has no lighttpd entry (this guide's inference from the table); Cloudflare Access documents self-hosted application and MFA policy controls; applying them to lighttpd is this guide's assessment. This guide assesses that a lighttpd origin reachable without passing through Access is not protected by it.", "components": ["docs", "authelia", "cloudflare-access"], "sources": ["docs:s1dbd1ce27da5", "authelia:s77083bc8faa2", "cloudflare-access:sfddb92e438ca", "cloudflare-access:s7cafe1b17afd", "cloudflare-access:sdcac732541da", "cloudflare-access:s603bdb6b24ab", "cloudflare-access:s863803bd9a1f"], "status": "REASONED"}, + "verify-config": {"text": "lighttpd 1.4.75 -tt -f /etc/lighttpd/lighttpd.conf tests the named file for syntax errors and initializes modules; configuration failures prevent the && reload (this guide's inference from the nonzero process result and shell chaining). The cited upstream unit starts from that file and defines a USR1 reload through ExecReload; systemctl reload triggers the service action, whose signal does not establish completion. Confirm the installed unit uses that file and defines reload (this guide's advice).", "components": ["docs", "lighttpd-verify-source", "systemd-reload"], "sources": ["docs:s6d5666b862a0", "lighttpd-verify-source:s0bbb4c861d5e", "lighttpd-verify-source:sca076bcd18dc", "lighttpd-verify-source:s9e69235fbbc7", "lighttpd-verify-source:s87104d328493", "lighttpd-verify-source:sba72bab46867", "lighttpd-verify-source:s2faa9b3149ac", "lighttpd-verify-source:s87b4b266bb0e", "lighttpd-verify-source:sa989ef425f1d", "lighttpd-verify-source:sf9d56b052c61", "systemd-reload:sb3f5069cd250", "systemd-reload:s441757b7048f"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "The HTTP header request should show a redirect to HTTPS; these are read-and-judge status checks.", "components": ["docs"], "sources": ["docs:s6de629323c64"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "An HTTPS request without credentials should return 401 once authentication is enabled.", "components": ["docs"], "sources": ["docs:s1dbd1ce27da5"], "status": "REASONED", "verify": [1]}, + "verify-listeners": {"text": "Read every listener with ss -tlnp: front-door authentication does not establish that no other listener serves content. The ss version is unrecorded.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED", "verify": [1]}, + "verify-host": {"text": "In lighttpd 1.4.75, request-time $HTTP[\"host\"] matching uses the HTTP Host value, while TLS SNI can select mod_openssl settings before request headers; a Host outside a conditional auth.require falls back to global configuration for that rule (this guide's inference from conditional merging and mod_auth documentation). On a protected path, this guide interprets 401 as the pass and 200 as the protected path served without authentication on this TLS connection to a client that sends another host. Apache 2.4.69 uses the SNI hostname, when the client sends it and the server supports SNI, for name-based TLS virtual-host selection; varying Host while keeping URL and SNI fixed is this guide's auth-check advice.", "components": ["docs", "lighttpd-verify-source", "apache-vhosts"], "sources": ["docs:s1dbd1ce27da5", "lighttpd-verify-source:s6a29b290d83a", "lighttpd-verify-source:s2fc4cf82abd9", "lighttpd-verify-source:s3f692a48bd01", "lighttpd-verify-source:sbb09703388c3", "lighttpd-verify-source:s8bb6bd7774f9", "lighttpd-verify-source:sb4c43fa1a807", "lighttpd-verify-source:s9e49b928cc81", "apache-vhosts:s558db45d4d61"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/nginx.md b/nginx.md index fb4e47790..73db0191b 100644 --- a/nginx.md +++ b/nginx.md @@ -1,199 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-17", - "documentation_checked": "2026-09", - "body_sha256": "07eda7c75b5cada2d56a730e2c7a33df34730eec5139f55d0545aad278723811", - "components": { - "ng": { - "name": "nginx documentation", - "basis": "unknown", - "sources": { - "s6a0728ca7609": "https://nginx.org/en/docs/http/configuring_https_servers.html", - "s40bdf1af1596": "https://nginx.org/en/docs/http/ngx_http_core_module.html", - "sb3bee9429629": "https://nginx.org/en/docs/http/ngx_http_limit_req_module.html", - "sddc43aac8779": "https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html", - "sf3430c5a0b22": "https://nginx.org/en/docs/http/ngx_http_proxy_module.html", - "sf32ce2956917": "https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html", - "sddacc1191f5f": "https://nginx.org/en/docs/http/ngx_http_realip_module.html" - } - }, - "policy": { - "name": "Mozilla TLS policy", - "basis": "unknown", - "sources": { - "s529b0eabe2ed": "https://ssl-config.mozilla.org/" - } - }, - "certbot": { - "name": "Certbot nginx configurator", - "basis": "v2.3.0", - "sources": { - "s3a02bfe1d7ad": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-nginx/certbot_nginx/_internal/configurator.py#L711" - } - }, - "hsts": { - "name": "HSTS specification", - "basis": "RFC 6797", - "sources": { - "s3f5af5f440f9": "https://www.rfc-editor.org/rfc/rfc6797.html#section-6.1.2" - } - }, - "nginx-source": { - "name": "nginx source", - "basis": "release-1.26.3", - "sources": { - "s0ae4e9bc8d07": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L216-L218", - "s002242c2d0cd": "https://github.com/nginx/nginx/blob/release-1.26.3/docs/xml/nginx/changes.xml#L470-L480", - "s305b7194043c": "https://github.com/nginx/nginx/blob/release-1.26.3/docs/xml/nginx/changes.xml#L739-L775", - "sbea253fd242b": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/v2/ngx_http_v2_module.c#L76-L81", - "s936193a42402": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/ngx_http_core_module.c#L4181-L4188", - "sac3cbbf7fc48": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L857-L862", - "s66c86979411e": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L153-L172", - "sabb195c173d5": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L788-L802", - "sa930199b563a": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L74-L80", - "seb1f8a9d91f9": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L666-L687", - "sf6eac8f6749d": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L788-L796", - "s3df6e64cd6a3": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_request_module.c#L135-L182", - "sced570e33c0f": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/ngx_http_request.h#L78-L138", - "sfafe27857e33": "https://github.com/nginx/nginx/blob/release-1.26.3/src/event/ngx_event_openssl.c#L4568-L4576" - } - }, - "authelia": { - "name": "Authelia nginx integration", - "basis": "v4.39.28", - "sources": { - "s279cd6449b53": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L39-L45", - "sdf01968ab996": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L245-L247", - "s237ae14bbb4a": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L517-L519" - } - }, - "oauth2-proxy": { - "name": "oauth2-proxy nginx integration", - "basis": "v7.15.4", - "sources": { - "sc62400f0ea8a": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/integrations/nginx.md#L16-L18" - } - }, - "cloudflare-access": { - "name": "Cloudflare Access web applications", - "basis": "unknown", - "sources": { - "sfe56578bedf4": "https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/" - } - }, - "httpd": { - "name": "Apache httpd source", - "basis": "2.4.69", - "sources": { - "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128", - "sf711558bdf3c": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L45", - "s1c1a101a70e7": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L290-L295", - "s4482c55d79a2": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L382" - } - }, - "debian-apache2": { - "name": "Debian apache2 packaging", - "basis": "2.4.69-1", - "sources": { - "sc2a26b3a7af5": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/control#L92-101", - "s62f6ed2866ab": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1" - } - }, - "owasp": { - "name": "OWASP Password Storage Cheat Sheet", - "basis": "commit 72865b1e", - "sources": { - "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" - } - } - }, - "claims": { - "certbot": {"text": "The guide says certbot --nginx edits the server block; certificate acquisition is delegated to the linked certificate guides.", "components": ["ng", "certbot"], "sources": ["ng:s6a0728ca7609", "certbot:s3a02bfe1d7ad"], "status": "REASONED"}, - "tls": {"text": "443 IPv4/IPv6 TLS uses certificate chain and private key, TLSv1.2/TLSv1.3 and ssl_prefer_server_ciphers off.", "components": ["ng"], "sources": ["ng:s6a0728ca7609"], "status": "REASONED"}, - "http2": {"text": "At nginx release-1.26.3, http2 on is the HTTP/2 module directive; nginx 1.25.1 introduced it and deprecated the older listen 443 ssl http2 parameter.", "components": ["ng", "nginx-source"], "sources": ["ng:s40bdf1af1596", "nginx-source:s002242c2d0cd", "nginx-source:sbea253fd242b", "nginx-source:s936193a42402"], "status": "REASONED"}, - "tokens": {"text": "server_tokens off suppresses nginx version in headers and error pages.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, - "hsts": {"text": "Send HSTS only after HTTPS works; includeSubDomains commits every subdomain for max-age.", "components": ["ng", "hsts", "nginx-source"], "sources": ["ng:s6a0728ca7609", "hsts:s3f5af5f440f9", "nginx-source:s0ae4e9bc8d07"], "status": "REASONED"}, - "hsts-inheritance": {"text": "At nginx release-1.26.3, a location add_header replaces inherited server headers; always changes response-code coverage, not inheritance.", "components": ["ng", "nginx-source"], "sources": ["ng:s40bdf1af1596", "nginx-source:s0ae4e9bc8d07", "nginx-source:seb1f8a9d91f9", "nginx-source:sf6eac8f6749d"], "status": "REASONED"}, - "proxy": {"text": "Proxy to loopback 127.0.0.1:3000 and set Host, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, - "forwarded-host": {"text": "Overwrite X-Forwarded-Host: client-set values otherwise pass upstream.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, - "cipher-policy": {"text": "Use Mozilla for explicit cipher policy; retain the stated protocol floor.", "components": ["ng", "policy"], "sources": ["ng:s6a0728ca7609", "policy:s529b0eabe2ed"], "status": "REASONED"}, - "tickets": {"text": "At nginx release-1.26.3, ssl_session_tickets defaults on; nginx 1.23.2 introduced automatic ticket-key rotation with a shared ssl_session_cache.", "components": ["ng", "nginx-source"], "sources": ["ng:s6a0728ca7609", "nginx-source:sac3cbbf7fc48", "nginx-source:s305b7194043c", "nginx-source:sfafe27857e33"], "status": "REASONED"}, - "redirect": {"text": "Port 80 on IPv4/IPv6 redirects with 301; a default server reflecting $host can redirect unmatched hosts off-site, so use 444 or a canonical host.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, - "password-file": {"text": "At httpd 2.4.69, htpasswd -B selects bcrypt, bare -B uses cost 5, and -C sets the cost; the OWASP cheat sheet at commit 72865b1e recommends at least 10 for legacy bcrypt. Debian apache2 2.4.69-1 packages htpasswd in apache2-utils.", "components": ["ng", "httpd", "owasp", "debian-apache2"], "sources": ["ng:sf32ce2956917", "debian-apache2:sc2a26b3a7af5", "debian-apache2:s62f6ed2866ab", "httpd:s309ecdd34379", "httpd:sf711558bdf3c", "httpd:s1c1a101a70e7", "httpd:s4482c55d79a2", "owasp:s4531e65b6502"], "status": "REASONED"}, - "basic": {"text": "auth_basic and auth_basic_user_file gate the existing proxied location over TLS; preserve its forwarded headers.", "components": ["ng"], "sources": ["ng:sf32ce2956917", "ng:sf3430c5a0b22"], "status": "REASONED"}, - "mtls": {"text": "At nginx release-1.26.3, ssl_client_certificate and ssl_verify_client on configure trusted client-certificate verification.", "components": ["ng", "nginx-source"], "sources": ["ng:s6a0728ca7609", "nginx-source:s66c86979411e", "nginx-source:sabb195c173d5", "nginx-source:sa930199b563a"], "status": "REASONED"}, - "mfa": {"text": "At nginx release-1.26.3, auth_request permits 2xx authentication subresponses and returns 401/403; Authelia v4.39.28 and oauth2-proxy v7.15.4 document nginx auth_request integration, and Cloudflare Access documents protecting self-hosted web applications; the nginx source cited here does not cover those integrations. MFA is this guide's requirement; whether a given installation enforces it depends on its configured portal or Access policy (this guide's assessment).", "components": ["ng", "nginx-source", "authelia", "oauth2-proxy", "cloudflare-access"], "sources": ["ng:sf32ce2956917", "nginx-source:s3df6e64cd6a3", "nginx-source:sced570e33c0f", "authelia:s279cd6449b53", "authelia:sdf01968ab996", "authelia:s237ae14bbb4a", "oauth2-proxy:sc62400f0ea8a", "cloudflare-access:sfe56578bedf4"], "status": "REASONED"}, - "body-limit": {"text": "client_max_body_size defaults to 1m; 10m rejects larger bodies with 413, and 0 disables the check.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, - "rate-limit": {"text": "Per-address limit_req uses a 10r/s zone, burst 20 and nodelay; rejection status defaults to 503.", "components": ["ng"], "sources": ["ng:sb3bee9429629"], "status": "REASONED"}, - "connection-limit": {"text": "limit_conn apiconn 10 caps concurrent connections per client address; it can also return 503.", "components": ["ng"], "sources": ["ng:sddc43aac8779"], "status": "REASONED"}, - "location-auth": {"text": "Keep limits in the authenticated location; sibling locations do not inherit its auth or proxy headers. Repeat auth or inherit it from server; auth_basic off is deliberate.", "components": ["ng"], "sources": ["ng:sf32ce2956917", "ng:s40bdf1af1596", "ng:sf3430c5a0b22"], "status": "REASONED"}, - "read-timeout": {"text": "proxy_read_timeout 60s limits gaps between upstream reads, not total response duration; active streams can continue indefinitely.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, - "client-address": {"text": "At a direct edge trust the appended last X-Forwarded-For entry; behind a CDN, recover the client with trusted set_real_ip_from and real_ip_header before per-client limits.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22", "ng:sddacc1191f5f", "ng:sb3bee9429629", "ng:sddc43aac8779"], "status": "REASONED"}, - "verify-config": {"text": "Run nginx -t before reload; no configuration-test outcome is recorded.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, - "verify-redirect": {"text": "HTTP should return 301 with an HTTPS Location.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, - "verify-tls": {"text": "HTTPS must validate without -k; TLS 1.1-only rejection must be protocol_version, not a generic/local failure; test TLS 1.0 separately.", "components": ["ng"], "sources": ["ng:s6a0728ca7609"], "status": "REASONED", "verify": [1]}, - "verify-auth": {"text": "An open path may return 200; the protected path must reject no credentials with 401/403, and valid credentials reach the app rather than 401.", "components": ["ng"], "sources": ["ng:sf32ce2956917"], "status": "REASONED", "verify": [1]}, - "verify-size": {"text": "Authenticated 9M succeeds normally only after raising the 1m default; 11M returns 413. Disable the check with 0 in isolation to distinguish backend refusal.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, - "verify-rate": {"text": "Concurrent unauthenticated probes exercise pre-auth limits; 503 does not identify the limiter. Compare both off and each alone with measured arrival rate.", "components": ["ng"], "sources": ["ng:sb3bee9429629", "ng:sddc43aac8779", "ng:sf32ce2956917"], "status": "REASONED", "verify": [1]}, - "verify-backend": {"text": "Inspect every listener: direct public access to backend 3000 bypasses proxy TLS/auth even when front-door probes pass.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED", "verify": [1]} - } -} ---- # nginx: TLS and authentication - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-17; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +Written against nginx source release-1.26.3, Certbot nginx configurator v2.3.0, Authelia nginx integration v4.39.28, oauth2-proxy nginx integration v7.15.4, Apache httpd source 2.4.69, Debian apache2 packaging 2.4.69-1, HSTS specification RFC 6797, and OWASP Password Storage Cheat Sheet commit 72865b1e; 3 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 28 claims, all reasoned from cited sources. -Components (recorded basis): - -- ng: nginx documentation (unknown) -- policy: Mozilla TLS policy (unknown) -- certbot: Certbot nginx configurator (v2.3.0) -- hsts: HSTS specification (RFC 6797) -- nginx-source: nginx source (release-1.26.3) -- authelia: Authelia nginx integration (v4.39.28) -- oauth2-proxy: oauth2-proxy nginx integration (v7.15.4) -- cloudflare-access: Cloudflare Access web applications (unknown) -- httpd: Apache httpd source (2.4.69) -- debian-apache2: Debian apache2 packaging (2.4.69-1) -- owasp: OWASP Password Storage Cheat Sheet (commit 72865b1e) - -Claims (full source mapping in the front matter): - -- certbot (REASONED; ng, certbot): The guide says certbot --nginx edits the server block; certificate acquisition is delegated to the linked certificate guides. -- tls (REASONED; ng): 443 IPv4/IPv6 TLS uses certificate chain and private key, TLSv1.2/TLSv1.3 and ssl_prefer_server_ciphers off. -- http2 (REASONED; ng, nginx-source): At nginx release-1.26.3, http2 on is the HTTP/2 module directive; nginx 1.25.1 introduced it and deprecated the older listen 443 ssl http2 parameter. -- tokens (REASONED; ng): server_tokens off suppresses nginx version in headers and error pages. -- hsts (REASONED; ng, hsts, nginx-source): Send HSTS only after HTTPS works; includeSubDomains commits every subdomain for max-age. -- hsts-inheritance (REASONED; ng, nginx-source): At nginx release-1.26.3, a location add_header replaces inherited server headers; always changes response-code coverage, not inheritance. -- proxy (REASONED; ng): Proxy to loopback 127.0.0.1:3000 and set Host, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host. -- forwarded-host (REASONED; ng): Overwrite X-Forwarded-Host: client-set values otherwise pass upstream. -- cipher-policy (REASONED; ng, policy): Use Mozilla for explicit cipher policy; retain the stated protocol floor. -- tickets (REASONED; ng, nginx-source): At nginx release-1.26.3, ssl_session_tickets defaults on; nginx 1.23.2 introduced automatic ticket-key rotation with a shared ssl_session_cache. -- redirect (REASONED; ng): Port 80 on IPv4/IPv6 redirects with 301; a default server reflecting $host can redirect unmatched hosts off-site, so use 444 or a canonical host. -- password-file (REASONED; ng, httpd, owasp, debian-apache2): At httpd 2.4.69, htpasswd -B selects bcrypt, bare -B uses cost 5, and -C sets the cost; the OWASP cheat sheet at commit 72865b1e recommends at least 10 for legacy bcrypt. Debian apache2 2.4.69-1 packages htpasswd in apache2-utils. -- basic (REASONED; ng): auth_basic and auth_basic_user_file gate the existing proxied location over TLS; preserve its forwarded headers. -- mtls (REASONED; ng, nginx-source): At nginx release-1.26.3, ssl_client_certificate and ssl_verify_client on configure trusted client-certificate verification. -- mfa (REASONED; ng, nginx-source, authelia, oauth2-proxy, cloudflare-access): At nginx release-1.26.3, auth_request permits 2xx authentication subresponses and returns 401/403; Authelia v4.39.28 and oauth2-proxy v7.15.4 document nginx auth_request integration, and Cloudflare Access documents protecting self-hosted web applications; the nginx source cited here does not cover those integrations. MFA is this guide's requirement; whether a given installation enforces it depends on its configured portal or Access policy (this guide's assessment). -- body-limit (REASONED; ng): client_max_body_size defaults to 1m; 10m rejects larger bodies with 413, and 0 disables the check. -- rate-limit (REASONED; ng): Per-address limit_req uses a 10r/s zone, burst 20 and nodelay; rejection status defaults to 503. -- connection-limit (REASONED; ng): limit_conn apiconn 10 caps concurrent connections per client address; it can also return 503. -- location-auth (REASONED; ng): Keep limits in the authenticated location; sibling locations do not inherit its auth or proxy headers. Repeat auth or inherit it from server; auth_basic off is deliberate. -- read-timeout (REASONED; ng): proxy_read_timeout 60s limits gaps between upstream reads, not total response duration; active streams can continue indefinitely. -- client-address (REASONED; ng): At a direct edge trust the appended last X-Forwarded-For entry; behind a CDN, recover the client with trusted set_real_ip_from and real_ip_header before per-client limits. -- verify-config (REASONED; ng): Run nginx -t before reload; no configuration-test outcome is recorded. -- verify-redirect (REASONED; ng): HTTP should return 301 with an HTTPS Location. -- verify-tls (REASONED; ng): HTTPS must validate without -k; TLS 1.1-only rejection must be protocol_version, not a generic/local failure; test TLS 1.0 separately. -- verify-auth (REASONED; ng): An open path may return 200; the protected path must reject no credentials with 401/403, and valid credentials reach the app rather than 401. -- verify-size (REASONED; ng): Authenticated 9M succeeds normally only after raising the 1m default; 11M returns 413. Disable the check with 0 in isolation to distinguish backend refusal. -- verify-rate (REASONED; ng): Concurrent unauthenticated probes exercise pre-auth limits; 503 does not identify the limiter. Compare both off and each alone with measured arrival rate. -- verify-backend (REASONED; ng): Inspect every listener: direct public access to backend 3000 bypasses proxy TLS/auth even when front-door probes pass. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Get a certificate first: [free-certificates.md](free-certificates.md) for a public host (note that `certbot --nginx` edits the server block for you), or [self-signed.md](self-signed.md) for internal use. @@ -425,3 +234,220 @@ ss -tlnp # read every listener; 3000: the app itself: 127.0.0.1 only, never 0. - Debian apache2 2.4.69-1 `apache2-utils` package description listing htpasswd (checked October 2026): https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/control#L92-101 - Debian apache2 packaging version 2.4.69-1 (checked October 2026): https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1 - OWASP Password Storage Cheat Sheet at commit 72865b1e (checked October 2026): https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/nginx.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- nginx-source: nginx source (release-1.26.3) +- ng: nginx documentation (unknown) +- certbot: Certbot nginx configurator (v2.3.0) +- authelia: Authelia nginx integration (v4.39.28) +- oauth2-proxy: oauth2-proxy nginx integration (v7.15.4) +- cloudflare-access: Cloudflare Access web applications (unknown) +- httpd: Apache httpd source (2.4.69) +- debian-apache2: Debian apache2 packaging (2.4.69-1) +- policy: Mozilla TLS policy (unknown) +- hsts: HSTS specification (RFC 6797) +- owasp: OWASP Password Storage Cheat Sheet (commit 72865b1e) + +Claims (full source mapping in the JSON block below): + +- certbot (REASONED; ng, certbot): The guide says certbot --nginx edits the server block; certificate acquisition is delegated to the linked certificate guides. +- tls (REASONED; ng): 443 IPv4/IPv6 TLS uses certificate chain and private key, TLSv1.2/TLSv1.3 and ssl_prefer_server_ciphers off. +- http2 (REASONED; ng, nginx-source): At nginx release-1.26.3, http2 on is the HTTP/2 module directive; nginx 1.25.1 introduced it and deprecated the older listen 443 ssl http2 parameter. +- tokens (REASONED; ng): server_tokens off suppresses nginx version in headers and error pages. +- hsts (REASONED; ng, hsts, nginx-source): Send HSTS only after HTTPS works; includeSubDomains commits every subdomain for max-age. +- hsts-inheritance (REASONED; ng, nginx-source): At nginx release-1.26.3, a location add_header replaces inherited server headers; always changes response-code coverage, not inheritance. +- proxy (REASONED; ng): Proxy to loopback 127.0.0.1:3000 and set Host, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host. +- forwarded-host (REASONED; ng): Overwrite X-Forwarded-Host: client-set values otherwise pass upstream. +- cipher-policy (REASONED; ng, policy): Use Mozilla for explicit cipher policy; retain the stated protocol floor. +- tickets (REASONED; ng, nginx-source): At nginx release-1.26.3, ssl_session_tickets defaults on; nginx 1.23.2 introduced automatic ticket-key rotation with a shared ssl_session_cache. +- redirect (REASONED; ng): Port 80 on IPv4/IPv6 redirects with 301; a default server reflecting $host can redirect unmatched hosts off-site, so use 444 or a canonical host. +- password-file (REASONED; ng, httpd, owasp, debian-apache2): At httpd 2.4.69, htpasswd -B selects bcrypt, bare -B uses cost 5, and -C sets the cost; the OWASP cheat sheet at commit 72865b1e recommends at least 10 for legacy bcrypt. Debian apache2 2.4.69-1 packages htpasswd in apache2-utils. +- basic (REASONED; ng): auth_basic and auth_basic_user_file gate the existing proxied location over TLS; preserve its forwarded headers. +- mtls (REASONED; ng, nginx-source): At nginx release-1.26.3, ssl_client_certificate and ssl_verify_client on configure trusted client-certificate verification. +- mfa (REASONED; ng, nginx-source, authelia, oauth2-proxy, cloudflare-access): At nginx release-1.26.3, auth_request permits 2xx authentication subresponses and returns 401/403; Authelia v4.39.28 and oauth2-proxy v7.15.4 document nginx auth_request integration, and Cloudflare Access documents protecting self-hosted web applications; the nginx source cited here does not cover those integrations. MFA is this guide's requirement; whether a given installation enforces it depends on its configured portal or Access policy (this guide's assessment). +- body-limit (REASONED; ng): client_max_body_size defaults to 1m; 10m rejects larger bodies with 413, and 0 disables the check. +- rate-limit (REASONED; ng): Per-address limit_req uses a 10r/s zone, burst 20 and nodelay; rejection status defaults to 503. +- connection-limit (REASONED; ng): limit_conn apiconn 10 caps concurrent connections per client address; it can also return 503. +- location-auth (REASONED; ng): Keep limits in the authenticated location; sibling locations do not inherit its auth or proxy headers. Repeat auth or inherit it from server; auth_basic off is deliberate. +- read-timeout (REASONED; ng): proxy_read_timeout 60s limits gaps between upstream reads, not total response duration; active streams can continue indefinitely. +- client-address (REASONED; ng): At a direct edge trust the appended last X-Forwarded-For entry; behind a CDN, recover the client with trusted set_real_ip_from and real_ip_header before per-client limits. +- verify-config (REASONED; ng): Run nginx -t before reload; no configuration-test outcome is recorded. +- verify-redirect (REASONED; ng): HTTP should return 301 with an HTTPS Location. +- verify-tls (REASONED; ng): HTTPS must validate without -k; TLS 1.1-only rejection must be protocol_version, not a generic/local failure; test TLS 1.0 separately. +- verify-auth (REASONED; ng): An open path may return 200; the protected path must reject no credentials with 401/403, and valid credentials reach the app rather than 401. +- verify-size (REASONED; ng): Authenticated 9M succeeds normally only after raising the 1m default; 11M returns 413. Disable the check with 0 in isolation to distinguish backend refusal. +- verify-rate (REASONED; ng): Concurrent unauthenticated probes exercise pre-auth limits; 503 does not identify the limiter. Compare both off and each alone with measured arrival rate. +- verify-backend (REASONED; ng): Inspect every listener: direct public access to backend 3000 bypasses proxy TLS/auth even when front-door probes pass. + +````json +{ + "schema": 1, + "checked": "2026-10-17", + "documentation_checked": "2026-09", + "body_sha256": "07eda7c75b5cada2d56a730e2c7a33df34730eec5139f55d0545aad278723811", + "components": { + "nginx-source": { + "name": "nginx source", + "basis": "release-1.26.3", + "sources": { + "s0ae4e9bc8d07": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L216-L218", + "s002242c2d0cd": "https://github.com/nginx/nginx/blob/release-1.26.3/docs/xml/nginx/changes.xml#L470-L480", + "s305b7194043c": "https://github.com/nginx/nginx/blob/release-1.26.3/docs/xml/nginx/changes.xml#L739-L775", + "sbea253fd242b": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/v2/ngx_http_v2_module.c#L76-L81", + "s936193a42402": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/ngx_http_core_module.c#L4181-L4188", + "sac3cbbf7fc48": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L857-L862", + "s66c86979411e": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L153-L172", + "sabb195c173d5": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L788-L802", + "sa930199b563a": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L74-L80", + "seb1f8a9d91f9": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L666-L687", + "sf6eac8f6749d": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L788-L796", + "s3df6e64cd6a3": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_request_module.c#L135-L182", + "sced570e33c0f": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/ngx_http_request.h#L78-L138", + "sfafe27857e33": "https://github.com/nginx/nginx/blob/release-1.26.3/src/event/ngx_event_openssl.c#L4568-L4576" + } + }, + "ng": { + "name": "nginx documentation", + "basis": "unknown", + "sources": { + "s6a0728ca7609": "https://nginx.org/en/docs/http/configuring_https_servers.html", + "s40bdf1af1596": "https://nginx.org/en/docs/http/ngx_http_core_module.html", + "sb3bee9429629": "https://nginx.org/en/docs/http/ngx_http_limit_req_module.html", + "sddc43aac8779": "https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html", + "sf3430c5a0b22": "https://nginx.org/en/docs/http/ngx_http_proxy_module.html", + "sf32ce2956917": "https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html", + "sddacc1191f5f": "https://nginx.org/en/docs/http/ngx_http_realip_module.html" + } + }, + "certbot": { + "name": "Certbot nginx configurator", + "basis": "v2.3.0", + "sources": { + "s3a02bfe1d7ad": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-nginx/certbot_nginx/_internal/configurator.py#L711" + } + }, + "authelia": { + "name": "Authelia nginx integration", + "basis": "v4.39.28", + "sources": { + "s279cd6449b53": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L39-L45", + "sdf01968ab996": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L245-L247", + "s237ae14bbb4a": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L517-L519" + } + }, + "oauth2-proxy": { + "name": "oauth2-proxy nginx integration", + "basis": "v7.15.4", + "sources": { + "sc62400f0ea8a": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/integrations/nginx.md#L16-L18" + } + }, + "cloudflare-access": { + "name": "Cloudflare Access web applications", + "basis": "unknown", + "sources": { + "sfe56578bedf4": "https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/" + } + }, + "httpd": { + "name": "Apache httpd source", + "basis": "2.4.69", + "sources": { + "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128", + "sf711558bdf3c": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L45", + "s1c1a101a70e7": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L290-L295", + "s4482c55d79a2": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L382" + } + }, + "debian-apache2": { + "name": "Debian apache2 packaging", + "basis": "2.4.69-1", + "sources": { + "sc2a26b3a7af5": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/control#L92-101", + "s62f6ed2866ab": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1" + } + }, + "policy": { + "name": "Mozilla TLS policy", + "basis": "unknown", + "sources": { + "s529b0eabe2ed": "https://ssl-config.mozilla.org/" + } + }, + "hsts": { + "name": "HSTS specification", + "basis": "RFC 6797", + "sources": { + "s3f5af5f440f9": "https://www.rfc-editor.org/rfc/rfc6797.html#section-6.1.2" + } + }, + "owasp": { + "name": "OWASP Password Storage Cheat Sheet", + "basis": "commit 72865b1e", + "sources": { + "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" + } + } + }, + "claims": { + "certbot": {"text": "The guide says certbot --nginx edits the server block; certificate acquisition is delegated to the linked certificate guides.", "components": ["ng", "certbot"], "sources": ["ng:s6a0728ca7609", "certbot:s3a02bfe1d7ad"], "status": "REASONED"}, + "tls": {"text": "443 IPv4/IPv6 TLS uses certificate chain and private key, TLSv1.2/TLSv1.3 and ssl_prefer_server_ciphers off.", "components": ["ng"], "sources": ["ng:s6a0728ca7609"], "status": "REASONED"}, + "http2": {"text": "At nginx release-1.26.3, http2 on is the HTTP/2 module directive; nginx 1.25.1 introduced it and deprecated the older listen 443 ssl http2 parameter.", "components": ["ng", "nginx-source"], "sources": ["ng:s40bdf1af1596", "nginx-source:s002242c2d0cd", "nginx-source:sbea253fd242b", "nginx-source:s936193a42402"], "status": "REASONED"}, + "tokens": {"text": "server_tokens off suppresses nginx version in headers and error pages.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, + "hsts": {"text": "Send HSTS only after HTTPS works; includeSubDomains commits every subdomain for max-age.", "components": ["ng", "hsts", "nginx-source"], "sources": ["ng:s6a0728ca7609", "hsts:s3f5af5f440f9", "nginx-source:s0ae4e9bc8d07"], "status": "REASONED"}, + "hsts-inheritance": {"text": "At nginx release-1.26.3, a location add_header replaces inherited server headers; always changes response-code coverage, not inheritance.", "components": ["ng", "nginx-source"], "sources": ["ng:s40bdf1af1596", "nginx-source:s0ae4e9bc8d07", "nginx-source:seb1f8a9d91f9", "nginx-source:sf6eac8f6749d"], "status": "REASONED"}, + "proxy": {"text": "Proxy to loopback 127.0.0.1:3000 and set Host, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, + "forwarded-host": {"text": "Overwrite X-Forwarded-Host: client-set values otherwise pass upstream.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, + "cipher-policy": {"text": "Use Mozilla for explicit cipher policy; retain the stated protocol floor.", "components": ["ng", "policy"], "sources": ["ng:s6a0728ca7609", "policy:s529b0eabe2ed"], "status": "REASONED"}, + "tickets": {"text": "At nginx release-1.26.3, ssl_session_tickets defaults on; nginx 1.23.2 introduced automatic ticket-key rotation with a shared ssl_session_cache.", "components": ["ng", "nginx-source"], "sources": ["ng:s6a0728ca7609", "nginx-source:sac3cbbf7fc48", "nginx-source:s305b7194043c", "nginx-source:sfafe27857e33"], "status": "REASONED"}, + "redirect": {"text": "Port 80 on IPv4/IPv6 redirects with 301; a default server reflecting $host can redirect unmatched hosts off-site, so use 444 or a canonical host.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, + "password-file": {"text": "At httpd 2.4.69, htpasswd -B selects bcrypt, bare -B uses cost 5, and -C sets the cost; the OWASP cheat sheet at commit 72865b1e recommends at least 10 for legacy bcrypt. Debian apache2 2.4.69-1 packages htpasswd in apache2-utils.", "components": ["ng", "httpd", "owasp", "debian-apache2"], "sources": ["ng:sf32ce2956917", "debian-apache2:sc2a26b3a7af5", "debian-apache2:s62f6ed2866ab", "httpd:s309ecdd34379", "httpd:sf711558bdf3c", "httpd:s1c1a101a70e7", "httpd:s4482c55d79a2", "owasp:s4531e65b6502"], "status": "REASONED"}, + "basic": {"text": "auth_basic and auth_basic_user_file gate the existing proxied location over TLS; preserve its forwarded headers.", "components": ["ng"], "sources": ["ng:sf32ce2956917", "ng:sf3430c5a0b22"], "status": "REASONED"}, + "mtls": {"text": "At nginx release-1.26.3, ssl_client_certificate and ssl_verify_client on configure trusted client-certificate verification.", "components": ["ng", "nginx-source"], "sources": ["ng:s6a0728ca7609", "nginx-source:s66c86979411e", "nginx-source:sabb195c173d5", "nginx-source:sa930199b563a"], "status": "REASONED"}, + "mfa": {"text": "At nginx release-1.26.3, auth_request permits 2xx authentication subresponses and returns 401/403; Authelia v4.39.28 and oauth2-proxy v7.15.4 document nginx auth_request integration, and Cloudflare Access documents protecting self-hosted web applications; the nginx source cited here does not cover those integrations. MFA is this guide's requirement; whether a given installation enforces it depends on its configured portal or Access policy (this guide's assessment).", "components": ["ng", "nginx-source", "authelia", "oauth2-proxy", "cloudflare-access"], "sources": ["ng:sf32ce2956917", "nginx-source:s3df6e64cd6a3", "nginx-source:sced570e33c0f", "authelia:s279cd6449b53", "authelia:sdf01968ab996", "authelia:s237ae14bbb4a", "oauth2-proxy:sc62400f0ea8a", "cloudflare-access:sfe56578bedf4"], "status": "REASONED"}, + "body-limit": {"text": "client_max_body_size defaults to 1m; 10m rejects larger bodies with 413, and 0 disables the check.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, + "rate-limit": {"text": "Per-address limit_req uses a 10r/s zone, burst 20 and nodelay; rejection status defaults to 503.", "components": ["ng"], "sources": ["ng:sb3bee9429629"], "status": "REASONED"}, + "connection-limit": {"text": "limit_conn apiconn 10 caps concurrent connections per client address; it can also return 503.", "components": ["ng"], "sources": ["ng:sddc43aac8779"], "status": "REASONED"}, + "location-auth": {"text": "Keep limits in the authenticated location; sibling locations do not inherit its auth or proxy headers. Repeat auth or inherit it from server; auth_basic off is deliberate.", "components": ["ng"], "sources": ["ng:sf32ce2956917", "ng:s40bdf1af1596", "ng:sf3430c5a0b22"], "status": "REASONED"}, + "read-timeout": {"text": "proxy_read_timeout 60s limits gaps between upstream reads, not total response duration; active streams can continue indefinitely.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, + "client-address": {"text": "At a direct edge trust the appended last X-Forwarded-For entry; behind a CDN, recover the client with trusted set_real_ip_from and real_ip_header before per-client limits.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22", "ng:sddacc1191f5f", "ng:sb3bee9429629", "ng:sddc43aac8779"], "status": "REASONED"}, + "verify-config": {"text": "Run nginx -t before reload; no configuration-test outcome is recorded.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP should return 301 with an HTTPS Location.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, + "verify-tls": {"text": "HTTPS must validate without -k; TLS 1.1-only rejection must be protocol_version, not a generic/local failure; test TLS 1.0 separately.", "components": ["ng"], "sources": ["ng:s6a0728ca7609"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "An open path may return 200; the protected path must reject no credentials with 401/403, and valid credentials reach the app rather than 401.", "components": ["ng"], "sources": ["ng:sf32ce2956917"], "status": "REASONED", "verify": [1]}, + "verify-size": {"text": "Authenticated 9M succeeds normally only after raising the 1m default; 11M returns 413. Disable the check with 0 in isolation to distinguish backend refusal.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, + "verify-rate": {"text": "Concurrent unauthenticated probes exercise pre-auth limits; 503 does not identify the limiter. Compare both off and each alone with measured arrival rate.", "components": ["ng"], "sources": ["ng:sb3bee9429629", "ng:sddc43aac8779", "ng:sf32ce2956917"], "status": "REASONED", "verify": [1]}, + "verify-backend": {"text": "Inspect every listener: direct public access to backend 3000 bypasses proxy TLS/auth even when front-door probes pass.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/nomad-consul.md b/nomad-consul.md index dd71a0200..dd8f15aa5 100644 --- a/nomad-consul.md +++ b/nomad-consul.md @@ -1,149 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-26", - "documentation_checked": "2026-09", - "body_sha256": "05ae6961d3225ec0e28ef043428dfeea38f86a7be4cfd93f6a6d58b4990f084a", - "components": { - "nomad": { - "name": "Nomad Community Edition", - "basis": "2.0.7", - "sources": { - "s52d80d2821ed": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/agent/config.go", - "sa1c2951d3ca6": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/config/ui.go", - "s553448c7957c": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/auth/auth.go", - "s7496bcf129dc": "https://github.com/hashicorp/nomad/blob/v2.0.7/acl/acl.go", - "sd665f15bc452": "https://github.com/hashicorp/nomad/blob/v2.0.7/drivers/exec/driver.go", - "sd5c86223bdf5": "https://github.com/hashicorp/nomad/blob/v2.0.7/client/config/config.go", - "sd5f2c389c6ae": "https://github.com/hashicorp/nomad/blob/v2.0.7/drivers/rawexec/driver.go", - "se7ba8e4262dc": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/config/tls.go", - "sc3e36e830b67": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/acl_endpoint.go", - "s754c582b7565": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/agent/agent.go", - "s8193264e48c2": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/acl.go", - "sba39c1a9119a": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/meta.go#L96", - "sa04bf8c95cb6": "https://github.com/hashicorp/nomad/blob/v2.0.7/api/api.go#L388-L389" - } - }, - "consul": { - "name": "Consul Community Edition", - "basis": "2.0.4", - "sources": { - "s679a306e1b08": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/config/default.go", - "s8a0b5abac641": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/config/builder.go", - "sc4fb30ca3702": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/consul/acl.go", - "s35b783d75794": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/acl_endpoint.go", - "sa77598ec3695": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/consul/acl_endpoint.go", - "s00748de44382": "https://github.com/hashicorp/consul/blob/v2.0.4/acl/acl.go", - "sab8a5866d9c9": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/http.go", - "sfeb50129fc10": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/token/persistence.go" - } - } - }, - "claims": { - "nomad-bind": {"text": "Nomad defaults bind_addr=0.0.0.0 and all three listeners fall back to it; setting only addresses.http leaves RPC and gossip on bind_addr.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, - "nomad-http": {"text": "Nomad HTTP API/UI uses 4646; UI defaults enabled at /ui/.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:sa1c2951d3ca6"], "status": "REASONED"}, - "nomad-rpc": {"text": "Nomad RPC uses 4647; keep it on private or management addresses.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, - "nomad-gossip": {"text": "Nomad Serf uses 4648 TCP/UDP; gossip is unencrypted until encrypt is configured.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, - "nomad-dev": {"text": "Linux -dev binds 127.0.0.1 except -dev-connect, which binds 0.0.0.0. Wildcard behavior was not observed.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, - "nomad-acl-default": {"text": "ACLs default disabled; enable acl.enabled on every agent before exposing the API.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s553448c7957c", "nomad:s7496bcf129dc"], "status": "REASONED"}, - "nomad-debug": {"text": "Debug endpoints still require enable_debug with ACLs off; -dev enables it.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s7496bcf129dc"], "status": "REASONED"}, - "nomad-exec": {"text": "On a root Linux client, exec uses a chroot and task user, default nobody, with root refused by the default exec denylist; no client job was run.", "components": ["nomad"], "sources": ["nomad:sd665f15bc452", "nomad:sd5c86223bdf5"], "status": "REASONED"}, - "nomad-raw-exec": {"text": "raw_exec has no isolation and defaults off, but -dev enables it.", "components": ["nomad"], "sources": ["nomad:sd5f2c389c6ae", "nomad:s52d80d2821ed"], "status": "REASONED"}, - "nomad-remote-exec": {"text": "Remote task execution defaults enabled through disable_remote_exec=false.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, - "nomad-tls": {"text": "HTTP/RPC TLS defaults off; enable both tls.http and tls.rpc.", "components": ["nomad"], "sources": ["nomad:se7ba8e4262dc"], "status": "REASONED"}, - "nomad-bootstrap-reset": {"text": "Unauthenticated PUT /v1/acl/bootstrap claims the first management token after ACL enablement; repeat is blocked until the operator reset file in data_dir/server.", "components": ["nomad"], "sources": ["nomad:sc3e36e830b67", "nomad:s754c582b7565"], "status": "REASONED"}, - "consul-client-bind": {"text": "Consul client_addr defaults 127.0.0.1; widening it or addresses.http makes the API reachable.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-http": {"text": "Consul HTTP API is 8500; UI defaults off and -dev enables /ui/ on that port.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-dns": {"text": "Consul DNS uses 8600 TCP/UDP on client_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-rpc": {"text": "Consul server RPC uses 8300 on bind_addr, default 0.0.0.0.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-lan": {"text": "Consul LAN Serf uses 8301 TCP/UDP on bind_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-wan": {"text": "Consul WAN Serf uses 8302 TCP/UDP on bind_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-https": {"text": "Consul HTTPS defaults disabled.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-grpc": {"text": "Plaintext gRPC defaults disabled; servers open TLS gRPC on 8503 on client_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-acl-default": {"text": "Consul ACLs default off and disabled ACL resolution grants management rights; with ACLs enabled default_policy still defaults allow.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641", "consul:sc4fb30ca3702"], "status": "REASONED"}, - "consul-bootstrap-reset": {"text": "Bootstrap is unauthenticated and one-time until an operator writes acl-bootstrap-reset; bootstrap privately before exposing the API.", "components": ["consul"], "sources": ["consul:s35b783d75794", "consul:sa77598ec3695"], "status": "REASONED"}, - "consul-script": {"text": "enable_script_checks defaults off; enabling without ACLs or allow_write_http_from only warns. Prefer enable_local_script_checks if needed.", "components": ["consul"], "sources": ["consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-remote-exec": {"text": "consul exec defaults disabled through disable_remote_exec=true.", "components": ["consul"], "sources": ["consul:s679a306e1b08"], "status": "REASONED"}, - "consul-tls-gossip": {"text": "TLS verification defaults off and gossip is unencrypted without encrypt.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "verify-inventory": {"text": "Inventory TCP/UDP on private or management addresses; wildcard exposed binds and actual external isolation remain reasoned.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [1]}, - "loopback-listeners": {"text": "Loopback runs used substitute TCP/Serf UDP ports; Consul DNS, WAN and gRPC were disabled, so their live listeners were not demonstrated.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "DEMONSTRATED", "evidence": "On the loopback run, the agents listened on TCP on test ports standing in for Nomad's 4646, 4647 and 4648 and Consul's 8500, 8300 and 8301 (the runs turned off Consul's DNS, Serf WAN and gRPC), and on UDP on the two Serf ports."}, - "nomad-acl-probe": {"text": "Loopback anonymous self/jobs/agent probes distinguished ACLs-off from ACLs-on; self remains 200 but changes AccessorID, while jobs and agent become 403.", "components": ["nomad"], "sources": ["nomad:s553448c7957c", "nomad:s7496bcf129dc", "nomad:s8193264e48c2"], "status": "DEMONSTRATED", "evidence": "All of these outcomes were observed with the curl probe block and these commands, on agents listening on test ports.", "verify": [2]}, - "nomad-agent-acl": {"text": "Server-handled paths can conceal a client with ACLs off; local /v1/agent/self exposed that client with 200 while the server returned 403.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s7496bcf129dc"], "status": "DEMONSTRATED", "evidence": "In a cluster whose server had ACLs on and was bootstrapped, a client agent configured with `acl { enabled = false }` answered the first two paths with the ACLs-on results and `/v1/agent/self` with `200`, while the server answered it with `403`.", "verify": [2]}, - "nomad-bootstrap-run": {"text": "ACL probe rejection did not prove bootstrap complete; first bootstrap issued management credentials and the second returned already-done.", "components": ["nomad"], "sources": ["nomad:sc3e36e830b67"], "status": "DEMONSTRATED", "evidence": "Before bootstrap, the probe already printed the ACLs-on results; the first `nomad acl bootstrap` printed a management token and the second failed with `Unexpected response code: 400 (ACL bootstrap already done ...)`."}, - "nomad-anonymous-policy": {"text": "Anonymous rights come from the named anonymous policy; historical exported-token inspection found it absent, then present with submit-job despite unchanged probe results.", "components": ["nomad"], "sources": ["nomad:s8193264e48c2"], "status": "DEMONSTRATED", "evidence": "After an `anonymous` policy granting only `submit-job` was applied, the probe's results did not change, an anonymous job registration returned `200`, and `nomad acl policy info anonymous`, run in the same earlier form, showed the policy."}, - "nomad-job-registration": {"text": "Anonymous registration succeeded with ACLs off and failed with ACLs on and no anonymous policy; registration is not a demonstrated job execution.", "components": ["nomad"], "sources": ["nomad:s553448c7957c", "nomad:s7496bcf129dc"], "status": "DEMONSTRATED", "evidence": "With ACLs off, an anonymous job registration also returned `200`; with ACLs on and no `anonymous` policy, it returned `403`."}, - "nomad-token-input": {"text": "CLI accepts -token or NOMAD_TOKEN, not stdin; the current prompted one-command prefix avoids argv/history but retains same-user/root environment visibility. That form has no recorded live Nomad run.", "components": ["nomad"], "sources": ["nomad:sba39c1a9119a", "nomad:sa04bf8c95cb6"], "status": "REASONED", "verify": [3]}, - "consul-acl-probe": {"text": "Loopback ACLs-off gave self 401/agent 200; enabled allow gave self 403/agent 200; enabled deny gave agent 403 with anonymous-permission text.", "components": ["consul"], "sources": ["consul:sc4fb30ca3702", "consul:s8a0b5abac641"], "status": "DEMONSTRATED", "evidence": "All of these outcomes were observed with this block and these commands, on agents listening on test ports.", "verify": [4]}, - "consul-agent-acl": {"text": "Each agent applies its own enabled/default_policy; mixed server/client configurations gave different results on their own APIs.", "components": ["consul"], "sources": ["consul:s8a0b5abac641", "consul:sc4fb30ca3702"], "status": "DEMONSTRATED", "evidence": "with the server on the `allow` policy and the client on `deny`, it printed `200` on `/v1/agent/self` against the server and the deny results against the client.", "verify": [4]}, - "consul-bootstrap-run": {"text": "Deny results appeared before bootstrap; first bootstrap issued a token and the second refused with bootstrap no longer allowed.", "components": ["consul"], "sources": ["consul:s35b783d75794", "consul:sa77598ec3695"], "status": "DEMONSTRATED", "evidence": "the first `consul acl bootstrap` printed a token and the second failed with `Unexpected response code: 403 (Permission denied: ACL bootstrap no longer allowed ...)`"}, - "consul-anonymous-token": {"text": "Inspect anonymous accessor 00000000-0000-0000-0000-000000000002 for policies, roles and identities; its recorded read listed none.", "components": ["consul"], "sources": ["consul:s00748de44382"], "status": "DEMONSTRATED", "evidence": "the anonymous token's read listed none of those sections."}, - "consul-default-token": {"text": "An agent default token substitutes for anonymous requests; recorded key-write default token yielded self 200 and agent 403 yet allowed an unauthenticated KV write.", "components": ["consul"], "sources": ["consul:sab8a5866d9c9", "consul:sfeb50129fc10"], "status": "DEMONSTRATED", "evidence": "On the loopback run, a default token holding key write, set through the agent's token API on a deny-policy agent, made the block print `200` on `/v1/acl/token/self` and a `403` on `/v1/agent/self` without the anonymous-token message, while a key-value write without a token succeeded.", "verify": [4]}, - "consul-kv": {"text": "Anonymous KV writes succeeded with ACLs off and allow policy, and were refused under deny.", "components": ["consul"], "sources": ["consul:sc4fb30ca3702", "consul:s679a306e1b08"], "status": "DEMONSTRATED", "evidence": "On the same runs an anonymous key-value write succeeded in both exposed states and was refused with `403` with the deny policy"}, - "consul-script-run": {"text": "With default script-check settings, remote script registration was refused.", "components": ["consul"], "sources": ["consul:s8a0b5abac641"], "status": "DEMONSTRATED", "evidence": "with script checks at their default, registering a script check through the API was refused with \"Scripts are disabled on this agent from remote calls\"."}, - "verify-tcp": {"text": "Probe each actual public IP with a known-open control; connected establishes reachability, refused/timeout only local nonreachability. This guide cross-references another guide's run; UDP is not tested.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [5]}, - "verify-address-limits": {"text": "TCP guard refuses zero/dotted-IPv6 forms but permits loopback and some hex forms reaching the probing host; use public addresses and corroborate surprising connections with host sockets.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [5]} - } -} ---- # Nomad and Consul: the scheduler and service-mesh APIs - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +Written against Nomad Community Edition 2.0.7 and Consul Community Edition 2.0.4. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 42 claims: 13 demonstrated by a check that ran and 29 reasoned from cited sources. -Metadata reviewed 2026-09-26; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- nomad: Nomad Community Edition (2.0.7) -- consul: Consul Community Edition (2.0.4) - -Claims (full source mapping in the front matter): - -- nomad-bind (REASONED; nomad): Nomad defaults bind_addr=0.0.0.0 and all three listeners fall back to it; setting only addresses.http leaves RPC and gossip on bind_addr. -- nomad-http (REASONED; nomad): Nomad HTTP API/UI uses 4646; UI defaults enabled at /ui/. -- nomad-rpc (REASONED; nomad): Nomad RPC uses 4647; keep it on private or management addresses. -- nomad-gossip (REASONED; nomad): Nomad Serf uses 4648 TCP/UDP; gossip is unencrypted until encrypt is configured. -- nomad-dev (REASONED; nomad): Linux -dev binds 127.0.0.1 except -dev-connect, which binds 0.0.0.0. Wildcard behavior was not observed. -- nomad-acl-default (REASONED; nomad): ACLs default disabled; enable acl.enabled on every agent before exposing the API. -- nomad-debug (REASONED; nomad): Debug endpoints still require enable_debug with ACLs off; -dev enables it. -- nomad-exec (REASONED; nomad): On a root Linux client, exec uses a chroot and task user, default nobody, with root refused by the default exec denylist; no client job was run. -- nomad-raw-exec (REASONED; nomad): raw_exec has no isolation and defaults off, but -dev enables it. -- nomad-remote-exec (REASONED; nomad): Remote task execution defaults enabled through disable_remote_exec=false. -- nomad-tls (REASONED; nomad): HTTP/RPC TLS defaults off; enable both tls.http and tls.rpc. -- nomad-bootstrap-reset (REASONED; nomad): Unauthenticated PUT /v1/acl/bootstrap claims the first management token after ACL enablement; repeat is blocked until the operator reset file in data_dir/server. -- consul-client-bind (REASONED; consul): Consul client_addr defaults 127.0.0.1; widening it or addresses.http makes the API reachable. -- consul-http (REASONED; consul): Consul HTTP API is 8500; UI defaults off and -dev enables /ui/ on that port. -- consul-dns (REASONED; consul): Consul DNS uses 8600 TCP/UDP on client_addr. -- consul-rpc (REASONED; consul): Consul server RPC uses 8300 on bind_addr, default 0.0.0.0. -- consul-lan (REASONED; consul): Consul LAN Serf uses 8301 TCP/UDP on bind_addr. -- consul-wan (REASONED; consul): Consul WAN Serf uses 8302 TCP/UDP on bind_addr. -- consul-https (REASONED; consul): Consul HTTPS defaults disabled. -- consul-grpc (REASONED; consul): Plaintext gRPC defaults disabled; servers open TLS gRPC on 8503 on client_addr. -- consul-acl-default (REASONED; consul): Consul ACLs default off and disabled ACL resolution grants management rights; with ACLs enabled default_policy still defaults allow. -- consul-bootstrap-reset (REASONED; consul): Bootstrap is unauthenticated and one-time until an operator writes acl-bootstrap-reset; bootstrap privately before exposing the API. -- consul-script (REASONED; consul): enable_script_checks defaults off; enabling without ACLs or allow_write_http_from only warns. Prefer enable_local_script_checks if needed. -- consul-remote-exec (REASONED; consul): consul exec defaults disabled through disable_remote_exec=true. -- consul-tls-gossip (REASONED; consul): TLS verification defaults off and gossip is unencrypted without encrypt. -- verify-inventory (REASONED; nomad, consul): Inventory TCP/UDP on private or management addresses; wildcard exposed binds and actual external isolation remain reasoned. -- loopback-listeners (DEMONSTRATED; nomad, consul): Loopback runs used substitute TCP/Serf UDP ports; Consul DNS, WAN and gRPC were disabled, so their live listeners were not demonstrated. -- nomad-acl-probe (DEMONSTRATED; nomad): Loopback anonymous self/jobs/agent probes distinguished ACLs-off from ACLs-on; self remains 200 but changes AccessorID, while jobs and agent become 403. -- nomad-agent-acl (DEMONSTRATED; nomad): Server-handled paths can conceal a client with ACLs off; local /v1/agent/self exposed that client with 200 while the server returned 403. -- nomad-bootstrap-run (DEMONSTRATED; nomad): ACL probe rejection did not prove bootstrap complete; first bootstrap issued management credentials and the second returned already-done. -- nomad-anonymous-policy (DEMONSTRATED; nomad): Anonymous rights come from the named anonymous policy; historical exported-token inspection found it absent, then present with submit-job despite unchanged probe results. -- nomad-job-registration (DEMONSTRATED; nomad): Anonymous registration succeeded with ACLs off and failed with ACLs on and no anonymous policy; registration is not a demonstrated job execution. -- nomad-token-input (REASONED; nomad): CLI accepts -token or NOMAD_TOKEN, not stdin; the current prompted one-command prefix avoids argv/history but retains same-user/root environment visibility. That form has no recorded live Nomad run. -- consul-acl-probe (DEMONSTRATED; consul): Loopback ACLs-off gave self 401/agent 200; enabled allow gave self 403/agent 200; enabled deny gave agent 403 with anonymous-permission text. -- consul-agent-acl (DEMONSTRATED; consul): Each agent applies its own enabled/default_policy; mixed server/client configurations gave different results on their own APIs. -- consul-bootstrap-run (DEMONSTRATED; consul): Deny results appeared before bootstrap; first bootstrap issued a token and the second refused with bootstrap no longer allowed. -- consul-anonymous-token (DEMONSTRATED; consul): Inspect anonymous accessor 00000000-0000-0000-0000-000000000002 for policies, roles and identities; its recorded read listed none. -- consul-default-token (DEMONSTRATED; consul): An agent default token substitutes for anonymous requests; recorded key-write default token yielded self 200 and agent 403 yet allowed an unauthenticated KV write. -- consul-kv (DEMONSTRATED; consul): Anonymous KV writes succeeded with ACLs off and allow policy, and were refused under deny. -- consul-script-run (DEMONSTRATED; consul): With default script-check settings, remote script registration was refused. -- verify-tcp (REASONED; nomad, consul): Probe each actual public IP with a known-open control; connected establishes reachability, refused/timeout only local nonreachability. This guide cross-references another guide's run; UDP is not tested. -- verify-address-limits (REASONED; nomad, consul): TCP guard refuses zero/dotted-IPv6 forms but permits loopback and some hex forms reaching the probing host; use public addresses and corroborate surprising connections with host sockets. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Nomad schedules and runs workloads, and Consul holds a cluster's service catalog, health checks and key-value store. Both are controlled through an HTTP API that also serves a web UI, and both ship @@ -449,3 +308,170 @@ give the host's public address. A "connected" on a port you did not mean to expo - Consul 2.0.4 default token for requests without one (`acl.tokens.default`, agent token API): https://github.com/hashicorp/consul/blob/v2.0.4/agent/http.go and https://github.com/hashicorp/consul/blob/v2.0.4/agent/token/persistence.go - Nomad 2.0.7 agent debug endpoints when ACLs are disabled (`enable_debug`): https://github.com/hashicorp/nomad/blob/v2.0.7/acl/acl.go - Consul 2.0.4 ACL bootstrap: https://github.com/hashicorp/consul/blob/v2.0.4/agent/acl_endpoint.go and https://github.com/hashicorp/consul/blob/v2.0.4/agent/consul/acl_endpoint.go + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/nomad-consul.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- nomad: Nomad Community Edition (2.0.7) +- consul: Consul Community Edition (2.0.4) + +Claims (full source mapping in the JSON block below): + +- nomad-bind (REASONED; nomad): Nomad defaults bind_addr=0.0.0.0 and all three listeners fall back to it; setting only addresses.http leaves RPC and gossip on bind_addr. +- nomad-http (REASONED; nomad): Nomad HTTP API/UI uses 4646; UI defaults enabled at /ui/. +- nomad-rpc (REASONED; nomad): Nomad RPC uses 4647; keep it on private or management addresses. +- nomad-gossip (REASONED; nomad): Nomad Serf uses 4648 TCP/UDP; gossip is unencrypted until encrypt is configured. +- nomad-dev (REASONED; nomad): Linux -dev binds 127.0.0.1 except -dev-connect, which binds 0.0.0.0. Wildcard behavior was not observed. +- nomad-acl-default (REASONED; nomad): ACLs default disabled; enable acl.enabled on every agent before exposing the API. +- nomad-debug (REASONED; nomad): Debug endpoints still require enable_debug with ACLs off; -dev enables it. +- nomad-exec (REASONED; nomad): On a root Linux client, exec uses a chroot and task user, default nobody, with root refused by the default exec denylist; no client job was run. +- nomad-raw-exec (REASONED; nomad): raw_exec has no isolation and defaults off, but -dev enables it. +- nomad-remote-exec (REASONED; nomad): Remote task execution defaults enabled through disable_remote_exec=false. +- nomad-tls (REASONED; nomad): HTTP/RPC TLS defaults off; enable both tls.http and tls.rpc. +- nomad-bootstrap-reset (REASONED; nomad): Unauthenticated PUT /v1/acl/bootstrap claims the first management token after ACL enablement; repeat is blocked until the operator reset file in data_dir/server. +- consul-client-bind (REASONED; consul): Consul client_addr defaults 127.0.0.1; widening it or addresses.http makes the API reachable. +- consul-http (REASONED; consul): Consul HTTP API is 8500; UI defaults off and -dev enables /ui/ on that port. +- consul-dns (REASONED; consul): Consul DNS uses 8600 TCP/UDP on client_addr. +- consul-rpc (REASONED; consul): Consul server RPC uses 8300 on bind_addr, default 0.0.0.0. +- consul-lan (REASONED; consul): Consul LAN Serf uses 8301 TCP/UDP on bind_addr. +- consul-wan (REASONED; consul): Consul WAN Serf uses 8302 TCP/UDP on bind_addr. +- consul-https (REASONED; consul): Consul HTTPS defaults disabled. +- consul-grpc (REASONED; consul): Plaintext gRPC defaults disabled; servers open TLS gRPC on 8503 on client_addr. +- consul-acl-default (REASONED; consul): Consul ACLs default off and disabled ACL resolution grants management rights; with ACLs enabled default_policy still defaults allow. +- consul-bootstrap-reset (REASONED; consul): Bootstrap is unauthenticated and one-time until an operator writes acl-bootstrap-reset; bootstrap privately before exposing the API. +- consul-script (REASONED; consul): enable_script_checks defaults off; enabling without ACLs or allow_write_http_from only warns. Prefer enable_local_script_checks if needed. +- consul-remote-exec (REASONED; consul): consul exec defaults disabled through disable_remote_exec=true. +- consul-tls-gossip (REASONED; consul): TLS verification defaults off and gossip is unencrypted without encrypt. +- verify-inventory (REASONED; nomad, consul): Inventory TCP/UDP on private or management addresses; wildcard exposed binds and actual external isolation remain reasoned. +- loopback-listeners (DEMONSTRATED; nomad, consul): Loopback runs used substitute TCP/Serf UDP ports; Consul DNS, WAN and gRPC were disabled, so their live listeners were not demonstrated. +- nomad-acl-probe (DEMONSTRATED; nomad): Loopback anonymous self/jobs/agent probes distinguished ACLs-off from ACLs-on; self remains 200 but changes AccessorID, while jobs and agent become 403. +- nomad-agent-acl (DEMONSTRATED; nomad): Server-handled paths can conceal a client with ACLs off; local /v1/agent/self exposed that client with 200 while the server returned 403. +- nomad-bootstrap-run (DEMONSTRATED; nomad): ACL probe rejection did not prove bootstrap complete; first bootstrap issued management credentials and the second returned already-done. +- nomad-anonymous-policy (DEMONSTRATED; nomad): Anonymous rights come from the named anonymous policy; historical exported-token inspection found it absent, then present with submit-job despite unchanged probe results. +- nomad-job-registration (DEMONSTRATED; nomad): Anonymous registration succeeded with ACLs off and failed with ACLs on and no anonymous policy; registration is not a demonstrated job execution. +- nomad-token-input (REASONED; nomad): CLI accepts -token or NOMAD_TOKEN, not stdin; the current prompted one-command prefix avoids argv/history but retains same-user/root environment visibility. That form has no recorded live Nomad run. +- consul-acl-probe (DEMONSTRATED; consul): Loopback ACLs-off gave self 401/agent 200; enabled allow gave self 403/agent 200; enabled deny gave agent 403 with anonymous-permission text. +- consul-agent-acl (DEMONSTRATED; consul): Each agent applies its own enabled/default_policy; mixed server/client configurations gave different results on their own APIs. +- consul-bootstrap-run (DEMONSTRATED; consul): Deny results appeared before bootstrap; first bootstrap issued a token and the second refused with bootstrap no longer allowed. +- consul-anonymous-token (DEMONSTRATED; consul): Inspect anonymous accessor 00000000-0000-0000-0000-000000000002 for policies, roles and identities; its recorded read listed none. +- consul-default-token (DEMONSTRATED; consul): An agent default token substitutes for anonymous requests; recorded key-write default token yielded self 200 and agent 403 yet allowed an unauthenticated KV write. +- consul-kv (DEMONSTRATED; consul): Anonymous KV writes succeeded with ACLs off and allow policy, and were refused under deny. +- consul-script-run (DEMONSTRATED; consul): With default script-check settings, remote script registration was refused. +- verify-tcp (REASONED; nomad, consul): Probe each actual public IP with a known-open control; connected establishes reachability, refused/timeout only local nonreachability. This guide cross-references another guide's run; UDP is not tested. +- verify-address-limits (REASONED; nomad, consul): TCP guard refuses zero/dotted-IPv6 forms but permits loopback and some hex forms reaching the probing host; use public addresses and corroborate surprising connections with host sockets. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "05ae6961d3225ec0e28ef043428dfeea38f86a7be4cfd93f6a6d58b4990f084a", + "components": { + "nomad": { + "name": "Nomad Community Edition", + "basis": "2.0.7", + "sources": { + "s52d80d2821ed": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/agent/config.go", + "sa1c2951d3ca6": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/config/ui.go", + "s553448c7957c": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/auth/auth.go", + "s7496bcf129dc": "https://github.com/hashicorp/nomad/blob/v2.0.7/acl/acl.go", + "sd665f15bc452": "https://github.com/hashicorp/nomad/blob/v2.0.7/drivers/exec/driver.go", + "sd5c86223bdf5": "https://github.com/hashicorp/nomad/blob/v2.0.7/client/config/config.go", + "sd5f2c389c6ae": "https://github.com/hashicorp/nomad/blob/v2.0.7/drivers/rawexec/driver.go", + "se7ba8e4262dc": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/config/tls.go", + "sc3e36e830b67": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/acl_endpoint.go", + "s754c582b7565": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/agent/agent.go", + "s8193264e48c2": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/acl.go", + "sba39c1a9119a": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/meta.go#L96", + "sa04bf8c95cb6": "https://github.com/hashicorp/nomad/blob/v2.0.7/api/api.go#L388-L389" + } + }, + "consul": { + "name": "Consul Community Edition", + "basis": "2.0.4", + "sources": { + "s679a306e1b08": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/config/default.go", + "s8a0b5abac641": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/config/builder.go", + "sc4fb30ca3702": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/consul/acl.go", + "s35b783d75794": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/acl_endpoint.go", + "sa77598ec3695": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/consul/acl_endpoint.go", + "s00748de44382": "https://github.com/hashicorp/consul/blob/v2.0.4/acl/acl.go", + "sab8a5866d9c9": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/http.go", + "sfeb50129fc10": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/token/persistence.go" + } + } + }, + "claims": { + "nomad-bind": {"text": "Nomad defaults bind_addr=0.0.0.0 and all three listeners fall back to it; setting only addresses.http leaves RPC and gossip on bind_addr.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-http": {"text": "Nomad HTTP API/UI uses 4646; UI defaults enabled at /ui/.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:sa1c2951d3ca6"], "status": "REASONED"}, + "nomad-rpc": {"text": "Nomad RPC uses 4647; keep it on private or management addresses.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-gossip": {"text": "Nomad Serf uses 4648 TCP/UDP; gossip is unencrypted until encrypt is configured.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-dev": {"text": "Linux -dev binds 127.0.0.1 except -dev-connect, which binds 0.0.0.0. Wildcard behavior was not observed.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-acl-default": {"text": "ACLs default disabled; enable acl.enabled on every agent before exposing the API.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s553448c7957c", "nomad:s7496bcf129dc"], "status": "REASONED"}, + "nomad-debug": {"text": "Debug endpoints still require enable_debug with ACLs off; -dev enables it.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s7496bcf129dc"], "status": "REASONED"}, + "nomad-exec": {"text": "On a root Linux client, exec uses a chroot and task user, default nobody, with root refused by the default exec denylist; no client job was run.", "components": ["nomad"], "sources": ["nomad:sd665f15bc452", "nomad:sd5c86223bdf5"], "status": "REASONED"}, + "nomad-raw-exec": {"text": "raw_exec has no isolation and defaults off, but -dev enables it.", "components": ["nomad"], "sources": ["nomad:sd5f2c389c6ae", "nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-remote-exec": {"text": "Remote task execution defaults enabled through disable_remote_exec=false.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-tls": {"text": "HTTP/RPC TLS defaults off; enable both tls.http and tls.rpc.", "components": ["nomad"], "sources": ["nomad:se7ba8e4262dc"], "status": "REASONED"}, + "nomad-bootstrap-reset": {"text": "Unauthenticated PUT /v1/acl/bootstrap claims the first management token after ACL enablement; repeat is blocked until the operator reset file in data_dir/server.", "components": ["nomad"], "sources": ["nomad:sc3e36e830b67", "nomad:s754c582b7565"], "status": "REASONED"}, + "consul-client-bind": {"text": "Consul client_addr defaults 127.0.0.1; widening it or addresses.http makes the API reachable.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-http": {"text": "Consul HTTP API is 8500; UI defaults off and -dev enables /ui/ on that port.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-dns": {"text": "Consul DNS uses 8600 TCP/UDP on client_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-rpc": {"text": "Consul server RPC uses 8300 on bind_addr, default 0.0.0.0.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-lan": {"text": "Consul LAN Serf uses 8301 TCP/UDP on bind_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-wan": {"text": "Consul WAN Serf uses 8302 TCP/UDP on bind_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-https": {"text": "Consul HTTPS defaults disabled.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-grpc": {"text": "Plaintext gRPC defaults disabled; servers open TLS gRPC on 8503 on client_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-acl-default": {"text": "Consul ACLs default off and disabled ACL resolution grants management rights; with ACLs enabled default_policy still defaults allow.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641", "consul:sc4fb30ca3702"], "status": "REASONED"}, + "consul-bootstrap-reset": {"text": "Bootstrap is unauthenticated and one-time until an operator writes acl-bootstrap-reset; bootstrap privately before exposing the API.", "components": ["consul"], "sources": ["consul:s35b783d75794", "consul:sa77598ec3695"], "status": "REASONED"}, + "consul-script": {"text": "enable_script_checks defaults off; enabling without ACLs or allow_write_http_from only warns. Prefer enable_local_script_checks if needed.", "components": ["consul"], "sources": ["consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-remote-exec": {"text": "consul exec defaults disabled through disable_remote_exec=true.", "components": ["consul"], "sources": ["consul:s679a306e1b08"], "status": "REASONED"}, + "consul-tls-gossip": {"text": "TLS verification defaults off and gossip is unencrypted without encrypt.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "verify-inventory": {"text": "Inventory TCP/UDP on private or management addresses; wildcard exposed binds and actual external isolation remain reasoned.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [1]}, + "loopback-listeners": {"text": "Loopback runs used substitute TCP/Serf UDP ports; Consul DNS, WAN and gRPC were disabled, so their live listeners were not demonstrated.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "DEMONSTRATED", "evidence": "On the loopback run, the agents listened on TCP on test ports standing in for Nomad's 4646, 4647 and 4648 and Consul's 8500, 8300 and 8301 (the runs turned off Consul's DNS, Serf WAN and gRPC), and on UDP on the two Serf ports."}, + "nomad-acl-probe": {"text": "Loopback anonymous self/jobs/agent probes distinguished ACLs-off from ACLs-on; self remains 200 but changes AccessorID, while jobs and agent become 403.", "components": ["nomad"], "sources": ["nomad:s553448c7957c", "nomad:s7496bcf129dc", "nomad:s8193264e48c2"], "status": "DEMONSTRATED", "evidence": "All of these outcomes were observed with the curl probe block and these commands, on agents listening on test ports.", "verify": [2]}, + "nomad-agent-acl": {"text": "Server-handled paths can conceal a client with ACLs off; local /v1/agent/self exposed that client with 200 while the server returned 403.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s7496bcf129dc"], "status": "DEMONSTRATED", "evidence": "In a cluster whose server had ACLs on and was bootstrapped, a client agent configured with `acl { enabled = false }` answered the first two paths with the ACLs-on results and `/v1/agent/self` with `200`, while the server answered it with `403`.", "verify": [2]}, + "nomad-bootstrap-run": {"text": "ACL probe rejection did not prove bootstrap complete; first bootstrap issued management credentials and the second returned already-done.", "components": ["nomad"], "sources": ["nomad:sc3e36e830b67"], "status": "DEMONSTRATED", "evidence": "Before bootstrap, the probe already printed the ACLs-on results; the first `nomad acl bootstrap` printed a management token and the second failed with `Unexpected response code: 400 (ACL bootstrap already done ...)`."}, + "nomad-anonymous-policy": {"text": "Anonymous rights come from the named anonymous policy; historical exported-token inspection found it absent, then present with submit-job despite unchanged probe results.", "components": ["nomad"], "sources": ["nomad:s8193264e48c2"], "status": "DEMONSTRATED", "evidence": "After an `anonymous` policy granting only `submit-job` was applied, the probe's results did not change, an anonymous job registration returned `200`, and `nomad acl policy info anonymous`, run in the same earlier form, showed the policy."}, + "nomad-job-registration": {"text": "Anonymous registration succeeded with ACLs off and failed with ACLs on and no anonymous policy; registration is not a demonstrated job execution.", "components": ["nomad"], "sources": ["nomad:s553448c7957c", "nomad:s7496bcf129dc"], "status": "DEMONSTRATED", "evidence": "With ACLs off, an anonymous job registration also returned `200`; with ACLs on and no `anonymous` policy, it returned `403`."}, + "nomad-token-input": {"text": "CLI accepts -token or NOMAD_TOKEN, not stdin; the current prompted one-command prefix avoids argv/history but retains same-user/root environment visibility. That form has no recorded live Nomad run.", "components": ["nomad"], "sources": ["nomad:sba39c1a9119a", "nomad:sa04bf8c95cb6"], "status": "REASONED", "verify": [3]}, + "consul-acl-probe": {"text": "Loopback ACLs-off gave self 401/agent 200; enabled allow gave self 403/agent 200; enabled deny gave agent 403 with anonymous-permission text.", "components": ["consul"], "sources": ["consul:sc4fb30ca3702", "consul:s8a0b5abac641"], "status": "DEMONSTRATED", "evidence": "All of these outcomes were observed with this block and these commands, on agents listening on test ports.", "verify": [4]}, + "consul-agent-acl": {"text": "Each agent applies its own enabled/default_policy; mixed server/client configurations gave different results on their own APIs.", "components": ["consul"], "sources": ["consul:s8a0b5abac641", "consul:sc4fb30ca3702"], "status": "DEMONSTRATED", "evidence": "with the server on the `allow` policy and the client on `deny`, it printed `200` on `/v1/agent/self` against the server and the deny results against the client.", "verify": [4]}, + "consul-bootstrap-run": {"text": "Deny results appeared before bootstrap; first bootstrap issued a token and the second refused with bootstrap no longer allowed.", "components": ["consul"], "sources": ["consul:s35b783d75794", "consul:sa77598ec3695"], "status": "DEMONSTRATED", "evidence": "the first `consul acl bootstrap` printed a token and the second failed with `Unexpected response code: 403 (Permission denied: ACL bootstrap no longer allowed ...)`"}, + "consul-anonymous-token": {"text": "Inspect anonymous accessor 00000000-0000-0000-0000-000000000002 for policies, roles and identities; its recorded read listed none.", "components": ["consul"], "sources": ["consul:s00748de44382"], "status": "DEMONSTRATED", "evidence": "the anonymous token's read listed none of those sections."}, + "consul-default-token": {"text": "An agent default token substitutes for anonymous requests; recorded key-write default token yielded self 200 and agent 403 yet allowed an unauthenticated KV write.", "components": ["consul"], "sources": ["consul:sab8a5866d9c9", "consul:sfeb50129fc10"], "status": "DEMONSTRATED", "evidence": "On the loopback run, a default token holding key write, set through the agent's token API on a deny-policy agent, made the block print `200` on `/v1/acl/token/self` and a `403` on `/v1/agent/self` without the anonymous-token message, while a key-value write without a token succeeded.", "verify": [4]}, + "consul-kv": {"text": "Anonymous KV writes succeeded with ACLs off and allow policy, and were refused under deny.", "components": ["consul"], "sources": ["consul:sc4fb30ca3702", "consul:s679a306e1b08"], "status": "DEMONSTRATED", "evidence": "On the same runs an anonymous key-value write succeeded in both exposed states and was refused with `403` with the deny policy"}, + "consul-script-run": {"text": "With default script-check settings, remote script registration was refused.", "components": ["consul"], "sources": ["consul:s8a0b5abac641"], "status": "DEMONSTRATED", "evidence": "with script checks at their default, registering a script check through the API was refused with \"Scripts are disabled on this agent from remote calls\"."}, + "verify-tcp": {"text": "Probe each actual public IP with a known-open control; connected establishes reachability, refused/timeout only local nonreachability. This guide cross-references another guide's run; UDP is not tested.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [5]}, + "verify-address-limits": {"text": "TCP guard refuses zero/dotted-IPv6 forms but permits loopback and some hex forms reaching the probing host; use public addresses and corroborate surprising connections with host sockets.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [5]} + } +} +```` + diff --git a/observability-components.md b/observability-components.md index 4ab97e4cd..36fae247d 100644 --- a/observability-components.md +++ b/observability-components.md @@ -1,6 +1,6 @@ # Self-hosted observability components: node_exporter, Alertmanager, Pushgateway, Jaeger, and Loki -Written against node_exporter v1.12.1, Alertmanager v0.34.1, Pushgateway v1.11.3, Jaeger v2.21.0, Loki v3.7.8, Exporter toolkit v0.17.1, OpenTelemetry configtls v1.66.0, and OpenTelemetry basicauth extension v0.160.0, plus 1 more recorded bases; 3 further sources are pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 59 claims: 14 demonstrated by a check that ran and 45 reasoned from cited sources. +Written against node_exporter v1.12.1, Alertmanager v0.34.1, Pushgateway v1.11.3, Jaeger v2.21.0, Loki v3.7.8, Exporter toolkit v0.17.1, OpenTelemetry configtls v1.66.0, and OpenTelemetry basicauth extension v0.160.0, plus 1 more recorded basis; 3 further sources are pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 59 claims: 14 demonstrated by a check that ran and 45 reasoned from cited sources. A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. diff --git a/plugin/skills/secure-deployment-config/references/MANIFEST.sha256 b/plugin/skills/secure-deployment-config/references/MANIFEST.sha256 index bde223d2c..adc90b199 100644 --- a/plugin/skills/secure-deployment-config/references/MANIFEST.sha256 +++ b/plugin/skills/secure-deployment-config/references/MANIFEST.sha256 @@ -1,7 +1,7 @@ 16d5e01fa7dc8355dd64b44f09923711070b47cd66686feef7e0bfe2adf63f2e README.md 0396b6b7ea6cd394bf147ada54efce3154d905ed8b40a36982eb5a1da0a98b2a controls-reference.md -4d0b98575e43b08c7e48fc43ff25808c2ef519e8c1463ea11e317199a694f8b4 free-certificates.md -5b574d7594d46b674e77bb1deceed3137489f0b0650f07475eff57bfa761ab72 self-signed.md +63dbab66d2ee6a851368cc772aff88eed98760b747b3735c746d3a8a828263e5 free-certificates.md +4e48558c75fe443cb3023ede38f621c4bfe5d966c6861ab57dd47ce89954a46f self-signed.md 86eca64127b4dca3510677732af318246214e54c8bcfd7662cf8d532520564ab cloudflare.md c63b3c46f99d8d1d5c60ef527994618c97a0300629692719ddba0e3eba4403d7 tailscale.md 57dc02171106069655b1ecf67ce77cd86ba505cd306469f48f544ed83482e845 tunnels.md @@ -15,20 +15,20 @@ a4bd1d507a1f15812717e44ed1426892aa789a7b3a955128945bd1930618c453 mfa.md 7eeae8824af359d1f95222759dfb152c38bfb44c5e0e20b79fd3c5c4de1898f8 cloud-identity-proxies.md 4e117acb25a74b2661a719c99da0ef8714108beaddfb56271716670d4092b1fd machine-auth.md f6cf36352de1b4acbed5850f7bec89dd40358e7f521507930e091f80d17915e8 fronting-auth.md -8a6515e4952389d459e63c277b280be9210ae0573628b6cd6e5ac5db12845b91 apache.md -a9a2db65552dc21482fdc9ba4ee9e5e03bdbb628154ac7e6e06a71f9f5ec63b8 nginx.md -987cf540fc9f27d3d04ef231e74bb75cbae4b046662d49c76bc6944c0a14c80b lighttpd.md -27eb98787e1ec79f9595b9623a9119f9f0469287e60927c9d980de958aa257e0 caddy.md -c476017eb77d93e355aea9b75ebc3cb80a23b416c6a256ae19b379aeb8acf5b5 haproxy.md -c99c2a44e1e38466e91a646fcfb66ed9ec80d7f57bbfea68c9e0af3b3a917039 traefik.md +56f8e29ff170027e675d042f22199b544b0bbf779b108926d18a900e222af1b5 apache.md +c8c900792380e7a1428660f9c3bcee688e760d59f6e1da55d9b6eb9f32619381 nginx.md +e294462aeb6ce1498c97d8198598f79b3b8d7c0930fad5538e687587cf76887b lighttpd.md +2faf447eb185932b48165a9492f8f90ee58ab8dcf21e82fbf3f2ad1e513758af caddy.md +123465d2fd41e0e4c7017bd8d71ca4ae11fc4df11110773ffac4080bc447c7ee haproxy.md +3ae6e4a8a38c6bd114821364c11b50468595278ffba1ac0715651a4d634325af traefik.md c49d2f39236c17b583cc1455b9665f647940609ecd79670e834d15753d263f3e nodejs.md 5fd9b83e3bec71386ffa2da831a70c57e0225aa1ca3d969a0737b8a28aaf5e62 python.md -674fabf5ccd8e290b06485ca5b46a4102091f70c5905655c4d955f6f637b1768 docker.md -9812d284347c4ede6beca1405e74cbf4d7bef964684e48a450f434566f92bffd kubernetes.md -f77b4c6c8fd3ae12480d7751d5a5e4b30d6f1b6decd080d299bbf82303450309 gitops-controllers.md -d2a89cacc8ff8bddd36967d11d975dd0360c00dbdb5bf9b31a210b5f9decb974 self-hosted-ci-runners.md +d03313bfa0afb1e9a960c070516b5f5f1d328d7b41aba215b52619f328395138 docker.md +1b8445e0dc25fb843829d1b2fdc2c67ee64b1e63b4a89b8518f1a994e722143f kubernetes.md +7c35f943796e0cfcba3c327a1c7c6e3dba385de4d90000a64ceaf09da81e5746 gitops-controllers.md +4d831688e4658d37dd10f2729397f3d225fdac03d8861f20a850a8ffc9962662 self-hosted-ci-runners.md b6e114b214938bb50a6bf20beb8ff87eab438a61bb328cc40abda0341d8e507e frontend-frameworks.md -b7bad14349ca3f07934272bbe1f88b6f26abbd55a1642419d4fe6b95c5007b78 container-hardening.md +3cc39b499774d04e366810d1aedd7e9c88dcf2534040d81c021d270bf37c2fb4 container-hardening.md b649f885ec96782415e287e83312fbd9444d0e70ab99467f477fcf9d29196671 nextjs.md 488b3e54ead7775aa87fc872568b05aa2a955535c64d1c117ead58fe06412494 go.md d3c5aa76a95c369cc8a3b4c16120d68cf5fa1512d1d88a3c9e3d2ddb27ed52fa dotnet.md @@ -74,7 +74,7 @@ cae5895e27a2a0d08773bf3c6f346c25deee35db50f15cfaaceff1c2545eba3e self-hosted-er 486162b144bf6326acaf70fbc3cc912d0534a3927d21f95a5a4d61a3ccb97b1c vector-databases.md 75560b7ae60244f00675674a45f3aaa1c715da1adb93f37334181d35924af9e4 mcp-servers.md 86c0dcec778ea45f122f458981177bbe1fe089867183613939570b2fde1fdff5 mcp-clients.md -b27badebb07b6e99d62e88c3aa748112bb85b8128a57cd3fc593983a3dd7daa6 ray.md +4ea7c2c7bc490c32fac988a7eca86353dd3b9edeca51bc805c4752e958bbb5e7 ray.md 0d4ebac99993632db53c1a3c835bf0cc8f113d689c2e1884b317b1427570651c mlflow.md b772a966100db0e2b6f1f7ba78903f1dde4fd6e351b58afb752124e55440681e agent-builders.md f205ad0512c2ad45bee20f9cce6faee7eaadddc16af066c413eee316ec79efa9 ai-infra-services.md @@ -82,22 +82,22 @@ f205ad0512c2ad45bee20f9cce6faee7eaadddc16af066c413eee316ec79efa9 ai-infra-servi 6b2981ef25b7920fae2233cb7fa2e8583b79d92af2aff8a715a585a0831dec60 image-gen-uis.md eedd2df7ea2844e3500579dddd9a18e57c746b7f49616bc76d9cdad0d20e235e chat-uis.md de45069e092247a3e60962e15ae9e775cfcc6520a983aaf91699332f94816311 llm-observability.md -fd733af42ee7aa2af3463fbc17f994b3053308d94f4683919d2b213df047e160 workflow-orchestrators.md +2d5dfb319feea0917c081bcace302d67e561c91bff34565a08c1dde251e3f320 workflow-orchestrators.md 6e808e21563913691d5e9b7f7b0ef457ad332ea9655a9e0a73bd454e4c27cc62 admin-uis.md -9a1ba5f778f4b2a3be609c27c1fedda92c1333fa3f00ece23053fa1084ce4750 observability-components.md +bd72febbec4b4aba89c866898fb3bc6f821985de356f733fdea356637f512cd8 observability-components.md 8d988fc846fb6a195061ad09ae70c9ebb50654ae8f499364086233554f5163d2 devops-uis.md -370aa067f9c7099af991a9962b2dca255e6e29d47b35302edc2f12a619b1e5b8 cors.md -f82a021a1bf4931f6daa1e78446d4707ceca65ecb5f5a05b95475299849949b2 headers.md -12684236218b4e669a9e21598b9b388fece93d5d09f2dae4860c80d3b3a5306f firebase-supabase.md +96807f276db704f93b2cf9f6a74462372e306bc727041a405e64a78e57e85dc8 cors.md +b342da74961426bb8757064ffadaace7a802f7ea9c90d357840821f2c724918f headers.md +7f6a4be63fe75218a7290360d7fa05e4f3eed04e759fd2279b8f9090ef8969d4 firebase-supabase.md 14a0db1c936d383f538b6c4be91a626020969dbd6bc5c59bafec40cdc6a8aa34 supabase-self-hosted.md e4ba632a7d23127a50f3ca43d06026acf696975a7eea07a4afd50709e6c41ebc web-exposure.md -25a2b7b0067d31825bf982b73c5ffd63e945ad8fc6d54cc7416d0685080351b1 realtime-webhooks.md +d47b8df006d058984f8d30c5fd9345fddb9dd5449d14da83047d0d1338d2be7c realtime-webhooks.md 1950768370917b08e8933c138b68c762b99dcc43bf3aa7059b23523d8504d61a transactional-email-posture.md c899ee328d6850c78dca030d5bb4253f3e5f037186e14bff89077992d37d11d8 bi-dashboards.md b336702b0aab97bc2e77ea0a67d20910f675b6c9f16b722f6e01b5dd0ada1b58 low-code-builders.md a8ee9cb4733666d9c13b241a92abe17df75a070295bb46a10eca9da7b17a813e server-admin-panels.md -ef2a792bda8518df2bf646532eb162c4aee733aad01907f37fc06d1a800249e3 nomad-consul.md -ce95a1682bab90a857e1faf8e1223ad401ed128d016de62edea9835a6f4606b8 pocketbase.md +57e33f986cc7430a583d4fa395fc97b46cc799414b95114a6fc66e7e46c02d26 nomad-consul.md +e2ed2544c21ba3583b6940b45e5503dbf7c22d0a2f4dab1b77244fdf203e2d6b pocketbase.md 31edb9401ef4e8f4374e2036f894e47c69d3888ffebb5bc921db05ae37136c52 headless-browser-services.md 884133e5681475d42f1a4ae96e0f4052bcad9dbca756091199e39c4e08356e6b headless-cms-instant-api.md 6edecfd9dd24bdaab2a9fa54f95201f451d9d3c2379454d818e0cae071a2aed7 exposure-index.md diff --git a/plugin/skills/secure-deployment-config/references/apache.md b/plugin/skills/secure-deployment-config/references/apache.md index 118738be8..6659c4fdd 100644 --- a/plugin/skills/secure-deployment-config/references/apache.md +++ b/plugin/skills/secure-deployment-config/references/apache.md @@ -1,201 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-18", - "documentation_checked": "2026-09", - "body_sha256": "645f3f5ab27a4df2af69e0680ac8e98f85ca416fe85a078c95075598e9b33f4d", - "components": { - "apache": { - "name": "Apache HTTP Server", - "basis": "2.4", - "sources": { - "sde155f1667f3": "https://httpd.apache.org/docs/2.4/ssl/ssl_howto.html", - "s99dcb49d86a4": "https://httpd.apache.org/docs/2.4/howto/auth.html", - "sc07f97af99e3": "https://httpd.apache.org/docs/2.4/vhosts/name-based.html", - "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy", - "s7c302c734a0d": "https://httpd.apache.org/docs/2.4/vhosts/details.html", - "s1dbb2636d050": "https://httpd.apache.org/docs/2.4/mod/mod_headers.html" - } - }, - "policy": { - "name": "Mozilla TLS policy", - "basis": "unknown", - "sources": { - "s529b0eabe2ed": "https://ssl-config.mozilla.org/" - } - }, - "tls-min": { - "name": "Apache TLSv1.3 minimum", - "basis": "2.4.36+", - "sources": { - "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" - } - }, - "sni-min": { - "name": "Apache per-vhost protocol minimum", - "basis": "2.4.42+", - "sources": { - "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" - } - }, - "openssl": { - "name": "OpenSSL qualification", - "basis": "1.1.1+", - "sources": { - "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" - } - }, - "chain-min": { - "name": "Apache chain-file minimum", - "basis": "2.4.8", - "sources": { - "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" - } - }, - "certbot": { - "name": "Certbot Apache installation", - "basis": "v2.3.0", - "sources": { - "sc74fcd0167d8": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L104-L106", - "sa453a66e4130": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/client.py#L646", - "sacf90ffa5a6d": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/client.py#L674-L677", - "s74412ad87016": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/constants.py#L66-L68" - } - }, - "debian-packaging": { - "name": "Debian apache2 packaging", - "basis": "2.4.69-1", - "sources": { - "s1c05297f1ef0": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.install#L1-7", - "s32bd234fb40a": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/config-dir/sites-available/default-ssl.conf#L1-4", - "sc9b152b1a444": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.links#L7", - "sd1c9de6b39aa": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/control#L92-102", - "s62f6ed2866ab": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1" - } - }, - "fedora-packaging": { - "name": "Fedora f44 httpd packaging", - "basis": "commit 6df70d39", - "sources": { - "s7d73f140a3fe": "https://src.fedoraproject.org/rpms/httpd/blob/6df70d392dace11a7c01ceffc5d8a929de0b7a35/f/httpd.spec#_204-212", - "s4479577a2e6a": "https://src.fedoraproject.org/rpms/httpd/blob/6df70d392dace11a7c01ceffc5d8a929de0b7a35/f/httpd.spec#_819-824" - } - }, - "htpasswd": { - "name": "httpd htpasswd", - "basis": "2.4.69", - "sources": { - "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128", - "s7850e1e9dfa1": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L43-L45", - "s1c1a101a70e7": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L290-L295" - } - }, - "owasp": { - "name": "OWASP password storage cheat sheet", - "basis": "commit 72865b1e", - "sources": { - "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" - } - }, - "authelia": { - "name": "Authelia proxy support", - "basis": "v4.39.28", - "sources": { - "s1de7fe72d011": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L112-L115" - } - }, - "cloudflare-access": { - "name": "Cloudflare Access documentation", - "basis": "commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104", - "sources": { - "sfddb92e438ca": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L14-L16", - "s7cafe1b17afd": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54", - "s603bdb6b24ab": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L45-L45", - "s863803bd9a1f": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L48-L48", - "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" - } - }, - "openidc": { - "name": "mod_auth_openidc", - "basis": "v2.4.20.3", - "sources": { - "s01e183d16f03": "https://github.com/OpenIDC/mod_auth_openidc/blob/v2.4.20.3/README.md#L11-L20" - } - } - }, - "claims": { - "certbot": {"text": "The guide says certbot --apache installs certificates and redirects by default; --redirect is explicit and --hsts defaults off.", "components": ["apache", "certbot"], "sources": ["apache:sde155f1667f3", "certbot:sc74fcd0167d8", "certbot:sa453a66e4130", "certbot:sacf90ffa5a6d", "certbot:s74412ad87016"], "status": "REASONED"}, - "modules": {"text": "Enable ssl/headers and a 443 vhost on Debian/Ubuntu, or install mod_ssl/httpd-tools on RHEL/Fedora; pinned packaging shows a2enmod, a2ensite (a link to a2enmod), default-ssl, and Fedora package names.", "components": ["apache", "debian-packaging", "fedora-packaging"], "sources": ["apache:sde155f1667f3", "debian-packaging:s62f6ed2866ab", "debian-packaging:s1c05297f1ef0", "debian-packaging:sc9b152b1a444", "debian-packaging:s32bd234fb40a", "fedora-packaging:s7d73f140a3fe"], "status": "REASONED"}, - "tls": {"text": "The 443 vhost enables TLS with a certificate chain and private key and TLSv1.2/TLSv1.3 only.", "components": ["apache"], "sources": ["apache:sde155f1667f3", "apache:scf804f4940d7"], "status": "REASONED"}, - "tls-version": {"text": "TLSv1.3 needs Apache 2.4.36+ with OpenSSL 1.1.1+; older builds use SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1.", "components": ["apache", "tls-min", "openssl"], "sources": ["apache:scf804f4940d7", "tls-min:scf804f4940d7", "openssl:scf804f4940d7"], "status": "REASONED"}, - "vhost-floor": {"text": "Independent per-vhost SSLProtocol needs Apache 2.4.42+, OpenSSL 1.1.1+ and client SNI; older builds also need the floor in global/base config.", "components": ["apache", "sni-min", "openssl"], "sources": ["apache:scf804f4940d7", "apache:s7c302c734a0d", "sni-min:scf804f4940d7", "openssl:scf804f4940d7"], "status": "REASONED"}, - "chain": {"text": "Apache 2.4.8+ permits the chain in SSLCertificateFile; SSLCertificateChainFile is deprecated.", "components": ["apache", "chain-min"], "sources": ["apache:scf804f4940d7", "chain-min:scf804f4940d7"], "status": "REASONED"}, - "hsts": {"text": "Header always sets HSTS after HTTPS works; includeSubDomains requires valid HTTPS everywhere.", "components": ["apache"], "sources": ["apache:sde155f1667f3", "apache:s1dbb2636d050"], "status": "REASONED"}, - "cipher-policy": {"text": "Generate explicit cipher policy with Mozilla rather than copying old lists.", "components": ["policy"], "sources": ["policy:s529b0eabe2ed"], "status": "REASONED"}, - "redirect": {"text": "The 80 vhost sends a permanent redirect to the canonical HTTPS host; retain only redirects and needed ACME HTTP-01 paths.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:sde155f1667f3"], "status": "REASONED"}, - "password-file": {"text": "Create a bcrypt htpasswd file with cost 12 and -c only for its first user; httpd 2.4.69 defaults bcrypt cost to 5 and OWASP recommends at least 10 for legacy bcrypt. Both password-file paths are guide choices.", "components": ["apache", "debian-packaging", "fedora-packaging", "htpasswd", "owasp"], "sources": ["apache:s99dcb49d86a4", "debian-packaging:s62f6ed2866ab", "debian-packaging:sd1c9de6b39aa", "fedora-packaging:s4479577a2e6a", "htpasswd:s309ecdd34379", "htpasswd:s7850e1e9dfa1", "htpasswd:s1c1a101a70e7", "owasp:s4531e65b6502"], "status": "REASONED"}, - "basic": {"text": "AuthType Basic, AuthName, AuthUserFile and Require valid-user protect the chosen site/path over TLS; enumerate every public path.", "components": ["apache"], "sources": ["apache:s99dcb49d86a4"], "status": "REASONED"}, - "mtls": {"text": "A separate machine-client 443 vhost requires a trusted certificate with SSLVerifyClient require and depth 2; SSLCACertificateFile is server/vhost scoped.", "components": ["apache"], "sources": ["apache:scf804f4940d7"], "status": "REASONED"}, - "mfa": {"text": "Basic is single-factor; Authelia v4.39.28 says Apache is unsupported for its proxy integration, and mod_auth_openidc v2.4.20.3 is an OIDC relying party. Provider MFA is guide advice; Cloudflare Access documents self-hosted application and MFA policy controls; fronting an Apache origin is this guide's suggested option and assessment. This guide assesses that an Apache origin reachable without passing through Access is not protected by it.", "components": ["apache", "authelia", "openidc", "cloudflare-access"], "sources": ["apache:s99dcb49d86a4", "authelia:s1de7fe72d011", "openidc:s01e183d16f03", "cloudflare-access:sfddb92e438ca", "cloudflare-access:s7cafe1b17afd", "cloudflare-access:sdcac732541da", "cloudflare-access:s603bdb6b24ab", "cloudflare-access:s863803bd9a1f"], "status": "REASONED"}, - "key-permissions": {"text": "Keep private keys root-owned and mode 0600.", "components": ["apache"], "sources": ["apache:sde155f1667f3"], "status": "REASONED"}, - "verify-config": {"text": "Run apachectl configtest before service reload; no outcome is recorded.", "components": ["apache"], "sources": ["apache:sde155f1667f3"], "status": "REASONED", "verify": [1]}, - "verify-redirect": {"text": "HTTP should return 301 with an HTTPS Location.", "components": ["apache"], "sources": ["apache:sc07f97af99e3"], "status": "REASONED", "verify": [1]}, - "verify-auth": {"text": "Read printed status: curl can exit 0 for 200 or 401; expect 200 before auth and 401 after it.", "components": ["apache"], "sources": ["apache:s99dcb49d86a4"], "status": "REASONED", "verify": [1]}, - "verify-listeners": {"text": "Inspect the entire listener table, without filtering away unexpected ports.", "components": ["apache"], "sources": ["apache:sc07f97af99e3"], "status": "REASONED", "verify": [1]}, - "verify-unmatched": {"text": "Use an unconfigured certificate-covered URL name for both SNI and Host; 401 passes, 200 exposes a protected resource through another vhost. Host-only probes can misleadingly give 401 or 421.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:scf804f4940d7", "apache:s7c302c734a0d"], "status": "REASONED", "verify": [1]}, - "verify-vhost-review": {"text": "If no spare certificate-covered name exists, use apachectl -S and review that the first matching vhost cannot serve the protected DocumentRoot.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:s7c302c734a0d"], "status": "REASONED", "verify": [2]} - } -} ---- # Apache HTTP Server: TLS and authentication - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-18; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- apache: Apache HTTP Server (2.4) -- policy: Mozilla TLS policy (unknown) -- tls-min: Apache TLSv1.3 minimum (2.4.36+) -- sni-min: Apache per-vhost protocol minimum (2.4.42+) -- openssl: OpenSSL qualification (1.1.1+) -- chain-min: Apache chain-file minimum (2.4.8) -- certbot: Certbot Apache installation (v2.3.0) -- debian-packaging: Debian apache2 packaging (2.4.69-1) -- fedora-packaging: Fedora f44 httpd packaging (commit 6df70d39) -- htpasswd: httpd htpasswd (2.4.69) -- owasp: OWASP password storage cheat sheet (commit 72865b1e) -- authelia: Authelia proxy support (v4.39.28) -- cloudflare-access: Cloudflare Access documentation (commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- openidc: mod_auth_openidc (v2.4.20.3) - -Claims (full source mapping in the front matter): +Written against Apache HTTP Server 2.4, Apache TLSv1.3 minimum 2.4.36+, Apache per-vhost protocol minimum 2.4.42+, Apache chain-file minimum 2.4.8, httpd htpasswd 2.4.69, OpenSSL qualification 1.1.1+, Certbot Apache installation v2.3.0, and Debian apache2 packaging 2.4.69-1, plus 5 more recorded bases; 1 further source carries no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 20 claims, all reasoned from cited sources. -- certbot (REASONED; apache, certbot): The guide says certbot --apache installs certificates and redirects by default; --redirect is explicit and --hsts defaults off. -- modules (REASONED; apache, debian-packaging, fedora-packaging): Enable ssl/headers and a 443 vhost on Debian/Ubuntu, or install mod_ssl/httpd-tools on RHEL/Fedora; pinned packaging shows a2enmod, a2ensite (a link to a2enmod), default-ssl, and Fedora package names. -- tls (REASONED; apache): The 443 vhost enables TLS with a certificate chain and private key and TLSv1.2/TLSv1.3 only. -- tls-version (REASONED; apache, tls-min, openssl): TLSv1.3 needs Apache 2.4.36+ with OpenSSL 1.1.1+; older builds use SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1. -- vhost-floor (REASONED; apache, sni-min, openssl): Independent per-vhost SSLProtocol needs Apache 2.4.42+, OpenSSL 1.1.1+ and client SNI; older builds also need the floor in global/base config. -- chain (REASONED; apache, chain-min): Apache 2.4.8+ permits the chain in SSLCertificateFile; SSLCertificateChainFile is deprecated. -- hsts (REASONED; apache): Header always sets HSTS after HTTPS works; includeSubDomains requires valid HTTPS everywhere. -- cipher-policy (REASONED; policy): Generate explicit cipher policy with Mozilla rather than copying old lists. -- redirect (REASONED; apache): The 80 vhost sends a permanent redirect to the canonical HTTPS host; retain only redirects and needed ACME HTTP-01 paths. -- password-file (REASONED; apache, debian-packaging, fedora-packaging, htpasswd, owasp): Create a bcrypt htpasswd file with cost 12 and -c only for its first user; httpd 2.4.69 defaults bcrypt cost to 5 and OWASP recommends at least 10 for legacy bcrypt. Both password-file paths are guide choices. -- basic (REASONED; apache): AuthType Basic, AuthName, AuthUserFile and Require valid-user protect the chosen site/path over TLS; enumerate every public path. -- mtls (REASONED; apache): A separate machine-client 443 vhost requires a trusted certificate with SSLVerifyClient require and depth 2; SSLCACertificateFile is server/vhost scoped. -- mfa (REASONED; apache, authelia, openidc, cloudflare-access): Basic is single-factor; Authelia v4.39.28 says Apache is unsupported for its proxy integration, and mod_auth_openidc v2.4.20.3 is an OIDC relying party. Provider MFA is guide advice; Cloudflare Access documents self-hosted application and MFA policy controls; fronting an Apache origin is this guide's suggested option and assessment. This guide assesses that an Apache origin reachable without passing through Access is not protected by it. -- key-permissions (REASONED; apache): Keep private keys root-owned and mode 0600. -- verify-config (REASONED; apache): Run apachectl configtest before service reload; no outcome is recorded. -- verify-redirect (REASONED; apache): HTTP should return 301 with an HTTPS Location. -- verify-auth (REASONED; apache): Read printed status: curl can exit 0 for 200 or 401; expect 200 before auth and 401 after it. -- verify-listeners (REASONED; apache): Inspect the entire listener table, without filtering away unexpected ports. -- verify-unmatched (REASONED; apache): Use an unconfigured certificate-covered URL name for both SNI and Host; 401 passes, 200 exposes a protected resource through another vhost. Host-only probes can misleadingly give 401 or 421. -- verify-vhost-review (REASONED; apache): If no spare certificate-covered name exists, use apachectl -S and review that the first matching vhost cannot serve the protected DocumentRoot. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Applies to Apache 2.4. Get a certificate first: [free-certificates.md](free-certificates.md) for a public host (note that `certbot --apache` obtains and installs the certificate and, at the time of writing, enables the HTTP-to-HTTPS redirect by default (`--redirect` requests it explicitly), but review its generated TLS config against steps 1 to 3 and add HSTS yourself, since certbot's `--hsts` is off by default), or [self-signed.md](self-signed.md) for internal use. @@ -353,3 +160,222 @@ sudo apachectl -S # the vhost list, in Apache's own matchi - Cloudflare Access origin reachability at commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L45-L45 ; https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L48-L48 - Cloudflare Access MFA policy controls at commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81 - mod_auth_openidc v2.4.20.3 relying-party description: https://github.com/OpenIDC/mod_auth_openidc/blob/v2.4.20.3/README.md#L11-L20 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/apache.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- apache: Apache HTTP Server (2.4) +- tls-min: Apache TLSv1.3 minimum (2.4.36+) +- sni-min: Apache per-vhost protocol minimum (2.4.42+) +- chain-min: Apache chain-file minimum (2.4.8) +- htpasswd: httpd htpasswd (2.4.69) +- openssl: OpenSSL qualification (1.1.1+) +- certbot: Certbot Apache installation (v2.3.0) +- debian-packaging: Debian apache2 packaging (2.4.69-1) +- fedora-packaging: Fedora f44 httpd packaging (commit 6df70d39) +- authelia: Authelia proxy support (v4.39.28) +- cloudflare-access: Cloudflare Access documentation (commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- openidc: mod_auth_openidc (v2.4.20.3) +- policy: Mozilla TLS policy (unknown) +- owasp: OWASP password storage cheat sheet (commit 72865b1e) + +Claims (full source mapping in the JSON block below): + +- certbot (REASONED; apache, certbot): The guide says certbot --apache installs certificates and redirects by default; --redirect is explicit and --hsts defaults off. +- modules (REASONED; apache, debian-packaging, fedora-packaging): Enable ssl/headers and a 443 vhost on Debian/Ubuntu, or install mod_ssl/httpd-tools on RHEL/Fedora; pinned packaging shows a2enmod, a2ensite (a link to a2enmod), default-ssl, and Fedora package names. +- tls (REASONED; apache): The 443 vhost enables TLS with a certificate chain and private key and TLSv1.2/TLSv1.3 only. +- tls-version (REASONED; apache, tls-min, openssl): TLSv1.3 needs Apache 2.4.36+ with OpenSSL 1.1.1+; older builds use SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1. +- vhost-floor (REASONED; apache, sni-min, openssl): Independent per-vhost SSLProtocol needs Apache 2.4.42+, OpenSSL 1.1.1+ and client SNI; older builds also need the floor in global/base config. +- chain (REASONED; apache, chain-min): Apache 2.4.8+ permits the chain in SSLCertificateFile; SSLCertificateChainFile is deprecated. +- hsts (REASONED; apache): Header always sets HSTS after HTTPS works; includeSubDomains requires valid HTTPS everywhere. +- cipher-policy (REASONED; policy): Generate explicit cipher policy with Mozilla rather than copying old lists. +- redirect (REASONED; apache): The 80 vhost sends a permanent redirect to the canonical HTTPS host; retain only redirects and needed ACME HTTP-01 paths. +- password-file (REASONED; apache, debian-packaging, fedora-packaging, htpasswd, owasp): Create a bcrypt htpasswd file with cost 12 and -c only for its first user; httpd 2.4.69 defaults bcrypt cost to 5 and OWASP recommends at least 10 for legacy bcrypt. Both password-file paths are guide choices. +- basic (REASONED; apache): AuthType Basic, AuthName, AuthUserFile and Require valid-user protect the chosen site/path over TLS; enumerate every public path. +- mtls (REASONED; apache): A separate machine-client 443 vhost requires a trusted certificate with SSLVerifyClient require and depth 2; SSLCACertificateFile is server/vhost scoped. +- mfa (REASONED; apache, authelia, openidc, cloudflare-access): Basic is single-factor; Authelia v4.39.28 says Apache is unsupported for its proxy integration, and mod_auth_openidc v2.4.20.3 is an OIDC relying party. Provider MFA is guide advice; Cloudflare Access documents self-hosted application and MFA policy controls; fronting an Apache origin is this guide's suggested option and assessment. This guide assesses that an Apache origin reachable without passing through Access is not protected by it. +- key-permissions (REASONED; apache): Keep private keys root-owned and mode 0600. +- verify-config (REASONED; apache): Run apachectl configtest before service reload; no outcome is recorded. +- verify-redirect (REASONED; apache): HTTP should return 301 with an HTTPS Location. +- verify-auth (REASONED; apache): Read printed status: curl can exit 0 for 200 or 401; expect 200 before auth and 401 after it. +- verify-listeners (REASONED; apache): Inspect the entire listener table, without filtering away unexpected ports. +- verify-unmatched (REASONED; apache): Use an unconfigured certificate-covered URL name for both SNI and Host; 401 passes, 200 exposes a protected resource through another vhost. Host-only probes can misleadingly give 401 or 421. +- verify-vhost-review (REASONED; apache): If no spare certificate-covered name exists, use apachectl -S and review that the first matching vhost cannot serve the protected DocumentRoot. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "645f3f5ab27a4df2af69e0680ac8e98f85ca416fe85a078c95075598e9b33f4d", + "components": { + "apache": { + "name": "Apache HTTP Server", + "basis": "2.4", + "sources": { + "sde155f1667f3": "https://httpd.apache.org/docs/2.4/ssl/ssl_howto.html", + "s99dcb49d86a4": "https://httpd.apache.org/docs/2.4/howto/auth.html", + "sc07f97af99e3": "https://httpd.apache.org/docs/2.4/vhosts/name-based.html", + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy", + "s7c302c734a0d": "https://httpd.apache.org/docs/2.4/vhosts/details.html", + "s1dbb2636d050": "https://httpd.apache.org/docs/2.4/mod/mod_headers.html" + } + }, + "tls-min": { + "name": "Apache TLSv1.3 minimum", + "basis": "2.4.36+", + "sources": { + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" + } + }, + "sni-min": { + "name": "Apache per-vhost protocol minimum", + "basis": "2.4.42+", + "sources": { + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" + } + }, + "chain-min": { + "name": "Apache chain-file minimum", + "basis": "2.4.8", + "sources": { + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" + } + }, + "htpasswd": { + "name": "httpd htpasswd", + "basis": "2.4.69", + "sources": { + "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128", + "s7850e1e9dfa1": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L43-L45", + "s1c1a101a70e7": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L290-L295" + } + }, + "openssl": { + "name": "OpenSSL qualification", + "basis": "1.1.1+", + "sources": { + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" + } + }, + "certbot": { + "name": "Certbot Apache installation", + "basis": "v2.3.0", + "sources": { + "sc74fcd0167d8": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L104-L106", + "sa453a66e4130": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/client.py#L646", + "sacf90ffa5a6d": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/client.py#L674-L677", + "s74412ad87016": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/constants.py#L66-L68" + } + }, + "debian-packaging": { + "name": "Debian apache2 packaging", + "basis": "2.4.69-1", + "sources": { + "s1c05297f1ef0": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.install#L1-7", + "s32bd234fb40a": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/config-dir/sites-available/default-ssl.conf#L1-4", + "sc9b152b1a444": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.links#L7", + "sd1c9de6b39aa": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/control#L92-102", + "s62f6ed2866ab": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1" + } + }, + "fedora-packaging": { + "name": "Fedora f44 httpd packaging", + "basis": "commit 6df70d39", + "sources": { + "s7d73f140a3fe": "https://src.fedoraproject.org/rpms/httpd/blob/6df70d392dace11a7c01ceffc5d8a929de0b7a35/f/httpd.spec#_204-212", + "s4479577a2e6a": "https://src.fedoraproject.org/rpms/httpd/blob/6df70d392dace11a7c01ceffc5d8a929de0b7a35/f/httpd.spec#_819-824" + } + }, + "authelia": { + "name": "Authelia proxy support", + "basis": "v4.39.28", + "sources": { + "s1de7fe72d011": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L112-L115" + } + }, + "cloudflare-access": { + "name": "Cloudflare Access documentation", + "basis": "commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "sfddb92e438ca": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L14-L16", + "s7cafe1b17afd": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54", + "s603bdb6b24ab": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L45-L45", + "s863803bd9a1f": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L48-L48", + "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" + } + }, + "openidc": { + "name": "mod_auth_openidc", + "basis": "v2.4.20.3", + "sources": { + "s01e183d16f03": "https://github.com/OpenIDC/mod_auth_openidc/blob/v2.4.20.3/README.md#L11-L20" + } + }, + "policy": { + "name": "Mozilla TLS policy", + "basis": "unknown", + "sources": { + "s529b0eabe2ed": "https://ssl-config.mozilla.org/" + } + }, + "owasp": { + "name": "OWASP password storage cheat sheet", + "basis": "commit 72865b1e", + "sources": { + "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" + } + } + }, + "claims": { + "certbot": {"text": "The guide says certbot --apache installs certificates and redirects by default; --redirect is explicit and --hsts defaults off.", "components": ["apache", "certbot"], "sources": ["apache:sde155f1667f3", "certbot:sc74fcd0167d8", "certbot:sa453a66e4130", "certbot:sacf90ffa5a6d", "certbot:s74412ad87016"], "status": "REASONED"}, + "modules": {"text": "Enable ssl/headers and a 443 vhost on Debian/Ubuntu, or install mod_ssl/httpd-tools on RHEL/Fedora; pinned packaging shows a2enmod, a2ensite (a link to a2enmod), default-ssl, and Fedora package names.", "components": ["apache", "debian-packaging", "fedora-packaging"], "sources": ["apache:sde155f1667f3", "debian-packaging:s62f6ed2866ab", "debian-packaging:s1c05297f1ef0", "debian-packaging:sc9b152b1a444", "debian-packaging:s32bd234fb40a", "fedora-packaging:s7d73f140a3fe"], "status": "REASONED"}, + "tls": {"text": "The 443 vhost enables TLS with a certificate chain and private key and TLSv1.2/TLSv1.3 only.", "components": ["apache"], "sources": ["apache:sde155f1667f3", "apache:scf804f4940d7"], "status": "REASONED"}, + "tls-version": {"text": "TLSv1.3 needs Apache 2.4.36+ with OpenSSL 1.1.1+; older builds use SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1.", "components": ["apache", "tls-min", "openssl"], "sources": ["apache:scf804f4940d7", "tls-min:scf804f4940d7", "openssl:scf804f4940d7"], "status": "REASONED"}, + "vhost-floor": {"text": "Independent per-vhost SSLProtocol needs Apache 2.4.42+, OpenSSL 1.1.1+ and client SNI; older builds also need the floor in global/base config.", "components": ["apache", "sni-min", "openssl"], "sources": ["apache:scf804f4940d7", "apache:s7c302c734a0d", "sni-min:scf804f4940d7", "openssl:scf804f4940d7"], "status": "REASONED"}, + "chain": {"text": "Apache 2.4.8+ permits the chain in SSLCertificateFile; SSLCertificateChainFile is deprecated.", "components": ["apache", "chain-min"], "sources": ["apache:scf804f4940d7", "chain-min:scf804f4940d7"], "status": "REASONED"}, + "hsts": {"text": "Header always sets HSTS after HTTPS works; includeSubDomains requires valid HTTPS everywhere.", "components": ["apache"], "sources": ["apache:sde155f1667f3", "apache:s1dbb2636d050"], "status": "REASONED"}, + "cipher-policy": {"text": "Generate explicit cipher policy with Mozilla rather than copying old lists.", "components": ["policy"], "sources": ["policy:s529b0eabe2ed"], "status": "REASONED"}, + "redirect": {"text": "The 80 vhost sends a permanent redirect to the canonical HTTPS host; retain only redirects and needed ACME HTTP-01 paths.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:sde155f1667f3"], "status": "REASONED"}, + "password-file": {"text": "Create a bcrypt htpasswd file with cost 12 and -c only for its first user; httpd 2.4.69 defaults bcrypt cost to 5 and OWASP recommends at least 10 for legacy bcrypt. Both password-file paths are guide choices.", "components": ["apache", "debian-packaging", "fedora-packaging", "htpasswd", "owasp"], "sources": ["apache:s99dcb49d86a4", "debian-packaging:s62f6ed2866ab", "debian-packaging:sd1c9de6b39aa", "fedora-packaging:s4479577a2e6a", "htpasswd:s309ecdd34379", "htpasswd:s7850e1e9dfa1", "htpasswd:s1c1a101a70e7", "owasp:s4531e65b6502"], "status": "REASONED"}, + "basic": {"text": "AuthType Basic, AuthName, AuthUserFile and Require valid-user protect the chosen site/path over TLS; enumerate every public path.", "components": ["apache"], "sources": ["apache:s99dcb49d86a4"], "status": "REASONED"}, + "mtls": {"text": "A separate machine-client 443 vhost requires a trusted certificate with SSLVerifyClient require and depth 2; SSLCACertificateFile is server/vhost scoped.", "components": ["apache"], "sources": ["apache:scf804f4940d7"], "status": "REASONED"}, + "mfa": {"text": "Basic is single-factor; Authelia v4.39.28 says Apache is unsupported for its proxy integration, and mod_auth_openidc v2.4.20.3 is an OIDC relying party. Provider MFA is guide advice; Cloudflare Access documents self-hosted application and MFA policy controls; fronting an Apache origin is this guide's suggested option and assessment. This guide assesses that an Apache origin reachable without passing through Access is not protected by it.", "components": ["apache", "authelia", "openidc", "cloudflare-access"], "sources": ["apache:s99dcb49d86a4", "authelia:s1de7fe72d011", "openidc:s01e183d16f03", "cloudflare-access:sfddb92e438ca", "cloudflare-access:s7cafe1b17afd", "cloudflare-access:sdcac732541da", "cloudflare-access:s603bdb6b24ab", "cloudflare-access:s863803bd9a1f"], "status": "REASONED"}, + "key-permissions": {"text": "Keep private keys root-owned and mode 0600.", "components": ["apache"], "sources": ["apache:sde155f1667f3"], "status": "REASONED"}, + "verify-config": {"text": "Run apachectl configtest before service reload; no outcome is recorded.", "components": ["apache"], "sources": ["apache:sde155f1667f3"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP should return 301 with an HTTPS Location.", "components": ["apache"], "sources": ["apache:sc07f97af99e3"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "Read printed status: curl can exit 0 for 200 or 401; expect 200 before auth and 401 after it.", "components": ["apache"], "sources": ["apache:s99dcb49d86a4"], "status": "REASONED", "verify": [1]}, + "verify-listeners": {"text": "Inspect the entire listener table, without filtering away unexpected ports.", "components": ["apache"], "sources": ["apache:sc07f97af99e3"], "status": "REASONED", "verify": [1]}, + "verify-unmatched": {"text": "Use an unconfigured certificate-covered URL name for both SNI and Host; 401 passes, 200 exposes a protected resource through another vhost. Host-only probes can misleadingly give 401 or 421.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:scf804f4940d7", "apache:s7c302c734a0d"], "status": "REASONED", "verify": [1]}, + "verify-vhost-review": {"text": "If no spare certificate-covered name exists, use apachectl -S and review that the first matching vhost cannot serve the protected DocumentRoot.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:s7c302c734a0d"], "status": "REASONED", "verify": [2]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/caddy.md b/plugin/skills/secure-deployment-config/references/caddy.md index 35759f8f1..1fa5e8fe7 100644 --- a/plugin/skills/secure-deployment-config/references/caddy.md +++ b/plugin/skills/secure-deployment-config/references/caddy.md @@ -1,206 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-27", - "documentation_checked": "2026-09", - "body_sha256": "7a5caab2cbcf90c693eab11084c5f8aebc6a3bc8e613f7ab76ca8167e3a9ea28", - "components": { - "admin": { - "name": "Caddy admin source", - "basis": "v2.11.4", - "sources": { - "s35044463fa67": "https://github.com/caddyserver/caddy/blob/v2.11.4/admin.go#L1433", - "s2bbcf239df02": "https://github.com/caddyserver/caddy/blob/v2.11.4/admin.go#L58-L67" - } - }, - "tags": { - "name": "Official image tag map", - "basis": "d82ca5102fa6735be29d5e1fc6ce03af77eb091e", - "sources": { - "sdc8aca42f5e2": "https://github.com/docker-library/official-images/blob/d82ca5102fa6735be29d5e1fc6ce03af77eb091e/library/caddy#L7-L65" - } - }, - "images": { - "name": "Caddy Docker source", - "basis": "fba2853501d36e8a72f946ac8cb7ff64d07e48f2", - "sources": { - "s0f0994ef9fac": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L17", - "s2dce0bbf3a70": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L59-L63", - "sd2c1b05a8088": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2022/Dockerfile#L27-L32", - "sf3e9c2d4e0cd": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2022/Dockerfile#L8", - "sc5dd3f9746fe": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2025/Dockerfile#L27-L32", - "s6c800a156eac": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2025/Dockerfile#L8", - "sc5c82704d33f": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2022/Dockerfile#L10", - "sc1cb0e1fb9eb": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2022/Dockerfile#L42-L47", - "s11528dfcc516": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2025/Dockerfile#L10", - "s70b76475a281": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2025/Dockerfile#L42-L47" - } - }, - "dist": { - "name": "Default Caddyfile source", - "basis": "33ae08ff08d168572df2956ed14fbc4949880d94", - "sources": { - "s433758a90af0": "https://github.com/caddyserver/dist/blob/33ae08ff08d168572df2956ed14fbc4949880d94/config/Caddyfile" - } - }, - "docker-network": { - "name": "Docker networking documentation", - "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", - "sources": { - "se1437c0f19fb": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L286", - "sfce645f8b46e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/drivers/host.md#L11-L13", - "sfd29f33e4108": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L302-L325" - } - }, - "docs-rolling": { - "name": "Caddy documentation (rolling)", - "basis": "unknown", - "sources": { - "s434e7b7d7f63": "https://caddyserver.com/docs/automatic-https", - "s3923477355fb": "https://caddyserver.com/docs/api", - "sdbcd496895d6": "https://caddyserver.com/docs/caddyfile/options", - "s31af5ed45a20": "https://caddyserver.com/docs/caddyfile/directives/request_body", - "sec59a61934c5": "https://caddyserver.com/docs/caddyfile/directives", - "sbc2bd9599bd1": "https://caddyserver.com/docs/caddyfile/directives/basic_auth", - "s2e47c3139bd2": "https://caddyserver.com/docs/caddyfile/directives/tls", - "s3865a73ccda3": "https://caddyserver.com/docs/caddyfile/directives/reverse_proxy", - "s82efc39a8130": "https://caddyserver.com/docs/conventions", - "s5a6da5adf385": "https://caddyserver.com/docs/caddyfile/directives/header", - "s8df93f3e3d4c": "https://caddyserver.com/docs/caddyfile/directives/forward_auth", - "sf12ad47e81b5": "https://caddyserver.com/docs/command-line#signals", - "s38b1b78ce980": "https://caddyserver.com/docs/caddyfile/matchers" - } - }, - "letsencrypt-notice": { - "name": "Let's Encrypt expiration-email notice", - "basis": "2025", - "sources": { - "sdf68595235d4": "https://letsencrypt.org/2025/01/22/ending-expiration-emails/" - } - }, - "service": { - "name": "caddyserver/dist service unit", - "basis": "33ae08ff08d168572df2956ed14fbc4949880d94", - "sources": { - "se70d276b03a1": "https://github.com/caddyserver/dist/blob/33ae08ff08d168572df2956ed14fbc4949880d94/init/caddy.service#L22-L32" - } - }, - "systemd": { - "name": "systemd manual", - "basis": "v262", - "sources": { - "see108b0e0bba": "https://github.com/systemd/systemd/blob/v262/man/systemd.exec.xml#L1558-L1660" - } - }, - "cloudflare": { - "name": "Cloudflare docs", - "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", - "sources": { - "s6f6104362214": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L17-L85", - "s81e5dd28d3e5": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json.mdx#L15-L17", - "sa7c8d635bfd5": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/partials/cloudflare-one/tunnel/origin-parameters.mdx#L196-L215" - } - }, - "ratelimit-source": { - "name": "caddy-ratelimit source", - "basis": "5625512f24f6f59d6f64fb3aafe5eecff0b286db", - "sources": { - "s093420cd3d2f": "https://raw.githubusercontent.com/mholt/caddy-ratelimit/5625512f24f6f59d6f64fb3aafe5eecff0b286db/README.md" - } - } - }, - "claims": { - "auto-https": {"text": "Caddy 2 obtains and renews public certificates and redirects HTTP to HTTPS; DNS must point here and the example requires public 80/443.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, - "issuers": {"text": "Public certificates come from Let's Encrypt or ZeroSSL and renew automatically.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, - "renewal-monitoring": {"text": "Set the ACME email contact but monitor renewal yourself: Let's Encrypt stopped expiration emails.", "components": ["docs-rolling", "letsencrypt-notice"], "sources": ["docs-rolling:sdbcd496895d6", "letsencrypt-notice:sdf68595235d4"], "status": "REASONED"}, - "backend": {"text": "reverse_proxy sends traffic to 127.0.0.1:3000; keep the app private to prevent TLS/auth bypass.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED"}, - "hsts": {"text": "Caddy sends no HSTS automatically; add it only after HTTPS works across every included subdomain.", "components": ["docs-rolling"], "sources": ["docs-rolling:s5a6da5adf385", "docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, - "forwarded": {"text": "Caddy ignores untrusted client X-Forwarded-* by default; configure trusted_proxies behind a trusted CDN/proxy.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED"}, - "internal-tls": {"text": "tls internal uses a local CA; clients must trust its root, with caddy trust for the local host. tls also accepts explicit cert/key files.", "components": ["docs-rolling"], "sources": ["docs-rolling:s2e47c3139bd2", "docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, - "basic": {"text": "basic_auth uses hashed passwords from caddy hash-password; it was named basicauth before v2.8.0 and remains single-factor.", "components": ["docs-rolling"], "sources": ["docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, - "path-auth": {"text": "Protect /admin and /admin/* together: path matches are exact and multiple paths are OR'ed.", "components": ["docs-rolling"], "sources": ["docs-rolling:s38b1b78ce980", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, - "mfa": {"text": "forward_auth (Caddy 2.5+) can delegate human authentication to Authelia; Cloudflare Access is a proposed layer that enforces MFA when its governing Access setting (policy over application over organization) requires it.", "components": ["docs-rolling", "cloudflare"], "sources": ["docs-rolling:s8df93f3e3d4c", "cloudflare:s6f6104362214"], "status": "REASONED"}, - "access-origin": {"text": "Cloudflare Access can require MFA when configured; the guide reasons that direct origin ingress can bypass Access, so restrict it; validate the Access JWT at the origin, which a tunnel's `access` setting has `cloudflared` do. Caddy still answers directly on 443.", "components": ["docs-rolling", "cloudflare"], "sources": ["docs-rolling:s3865a73ccda3", "cloudflare:s6f6104362214", "cloudflare:s81e5dd28d3e5", "cloudflare:sa7c8d635bfd5"], "status": "REASONED"}, - "body-limit": {"text": "Add request_body max_size 10MB inside the existing authenticated site; replacing the site without basic_auth makes it public.", "components": ["docs-rolling"], "sources": ["docs-rolling:s31af5ed45a20", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, - "rate-limit": {"text": "The standard build has no rate_limit directive; use a compiled community caddy-ratelimit module or an upstream layer.", "components": ["docs-rolling", "ratelimit-source"], "sources": ["docs-rolling:sec59a61934c5", "ratelimit-source:s093420cd3d2f"], "status": "REASONED"}, - "admin-bind": {"text": "The admin API defaults to localhost:2019 as of v2.11.4; never publish it or reverse-proxy to it.", "components": ["admin", "docs-rolling"], "sources": ["admin:s35044463fa67", "admin:s2bbcf239df02", "docs-rolling:s3923477355fb"], "status": "REASONED"}, - "admin-authority": {"text": "Reachable unauthenticated admin clients can replace config at POST /load, edit /config/ or stop with POST /stop.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb"], "status": "REASONED"}, - "admin-isolation": {"text": "Host/Origin checks are not process isolation; local untrusted workloads require a permissioned Unix socket.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "docs-rolling:sdbcd496895d6"], "status": "REASONED"}, - "docker-admin": {"text": "The official 2.11.4 Alpine, Windows Server Core 2022/2025, and Nano Server 2022/2025 runtime tags map to fba28535 Dockerfiles that EXPOSE 2019 and run the default Caddyfile without admin; absent overrides, the endpoint stays namespace-local.", "components": ["tags", "images", "dist", "admin"], "sources": ["tags:sdc8aca42f5e2", "images:s0f0994ef9fac", "images:s2dce0bbf3a70", "images:sd2c1b05a8088", "images:sf3e9c2d4e0cd", "images:s6c800a156eac", "images:sc5c82704d33f", "images:sc1cb0e1fb9eb", "images:s11528dfcc516", "images:s70b76475a281", "dist:s433758a90af0", "admin:s35044463fa67", "admin:s2bbcf239df02", "images:sc5dd3f9746fe"], "status": "REASONED"}, - "admin-override": {"text": "A mounted admin setting or CADDY_ADMIN changes the address; do not set 0.0.0.0:2019 to make publication work.", "components": ["admin", "dist", "docs-rolling"], "sources": ["docs-rolling:sdbcd496895d6", "admin:s35044463fa67", "admin:s2bbcf239df02", "dist:s433758a90af0"], "status": "REASONED"}, - "namespace": {"text": "The shipped admin default is localhost:2019. On Docker Engine on Linux, bridge networking keeps loopback container-local; host or container networking shares a namespace, so treat its processes as potential admin clients.", "components": ["images", "dist", "admin", "docker-network", "docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "images:s0f0994ef9fac", "images:s2dce0bbf3a70", "images:sd2c1b05a8088", "images:sf3e9c2d4e0cd", "images:sc5dd3f9746fe", "images:s6c800a156eac", "images:sc5c82704d33f", "images:sc1cb0e1fb9eb", "images:s11528dfcc516", "images:s70b76475a281", "dist:s433758a90af0", "admin:s35044463fa67", "docker-network:se1437c0f19fb", "docker-network:sfce645f8b46e", "docker-network:sfd29f33e4108"], "status": "REASONED"}, - "admin-socket": {"text": "admin unix//run/caddy/admin.sock uses owner-only mode 0200 by default; append |0600 to set it explicitly.", "components": ["docs-rolling"], "sources": ["docs-rolling:sdbcd496895d6", "docs-rolling:s82efc39a8130"], "status": "REASONED"}, - "runtime-directory": {"text": "The dist service at 33ae08ff runs as caddy without RuntimeDirectory; systemd v262 creates /run/caddy for RuntimeDirectory=caddy at service start and owns it as caddy. Restart for the socket switch is guide reasoning.", "components": ["docs-rolling", "service", "systemd"], "sources": ["docs-rolling:sdbcd496895d6", "service:se70d276b03a1", "systemd:see108b0e0bba"], "status": "REASONED"}, - "reload": {"text": "The dist service at 33ae08ff uses caddy reload --config /etc/caddy/Caddyfile --force; API reload uses the Caddyfile admin address. With admin off, guide reasoning requires systemd restart.", "components": ["docs-rolling", "service"], "sources": ["docs-rolling:sdbcd496895d6", "docs-rolling:sf12ad47e81b5", "service:se70d276b03a1"], "status": "REASONED"}, - "signal-reload": {"text": "A manual run without --resume may reload startup config via SIGUSR1; API changes or reload with a different file/adapter disable signal reloads.", "components": ["docs-rolling"], "sources": ["docs-rolling:sf12ad47e81b5"], "status": "REASONED"}, - "on-demand": {"text": "Do not enable unrestricted on_demand TLS without an ask endpoint or host allowlist; this guide does not enable it.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, - "verify-config": {"text": "Validate /etc/caddy/Caddyfile; no validation outcome is recorded.", "components": ["docs-rolling"], "sources": ["docs-rolling:sf12ad47e81b5"], "status": "REASONED", "verify": [1]}, - "verify-redirect": {"text": "HTTP should redirect to HTTPS.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED", "verify": [1]}, - "verify-auth": {"text": "With site-wide auth, HTTPS without credentials returns 401.", "components": ["docs-rolling"], "sources": ["docs-rolling:sbc2bd9599bd1"], "status": "REASONED", "verify": [1]}, - "verify-scope": {"text": "The @admin variant must return 401 for /admin and /admin/x, but an app response rather than 401 for a non-admin path.", "components": ["docs-rolling"], "sources": ["docs-rolling:s38b1b78ce980", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED", "verify": [1]}, - "verify-size": {"text": "Authenticated 1M reaches the app and 11M returns 413; 401/000 void the control. Auth can prevent body reads; remove request_body in isolation to attribute refusal.", "components": ["docs-rolling"], "sources": ["docs-rolling:s31af5ed45a20", "docs-rolling:sbc2bd9599bd1", "docs-rolling:sec59a61934c5"], "status": "REASONED", "verify": [1]}, - "verify-backend": {"text": "Read all listeners: app 3000 must be loopback-only even if proxy checks pass.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED", "verify": [1]}, - "verify-admin": {"text": "TCP admin must bind only loopback; absent 2019 requires confirming admin off or a socket. Check Unix socket existence and owner restrictions separately.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "docs-rolling:sdbcd496895d6", "docs-rolling:s82efc39a8130"], "status": "REASONED", "verify": [1]} - } -} ---- # Caddy: TLS and authentication - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +Written against Caddy admin source v2.11.4, systemd manual v262, and Let's Encrypt expiration-email notice 2025; 7 further sources are pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-09-27; documentation checked September 2026. The guide records 31 claims, all reasoned from cited sources. -Metadata reviewed 2026-09-27; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- admin: Caddy admin source (v2.11.4) -- tags: Official image tag map (d82ca5102fa6735be29d5e1fc6ce03af77eb091e) -- images: Caddy Docker source (fba2853501d36e8a72f946ac8cb7ff64d07e48f2) -- dist: Default Caddyfile source (33ae08ff08d168572df2956ed14fbc4949880d94) -- docker-network: Docker networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- docs-rolling: Caddy documentation (rolling) (unknown) -- letsencrypt-notice: Let's Encrypt expiration-email notice (2025) -- service: caddyserver/dist service unit (33ae08ff08d168572df2956ed14fbc4949880d94) -- systemd: systemd manual (v262) -- cloudflare: Cloudflare docs (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- ratelimit-source: caddy-ratelimit source (5625512f24f6f59d6f64fb3aafe5eecff0b286db) - -Claims (full source mapping in the front matter): - -- auto-https (REASONED; docs-rolling): Caddy 2 obtains and renews public certificates and redirects HTTP to HTTPS; DNS must point here and the example requires public 80/443. -- issuers (REASONED; docs-rolling): Public certificates come from Let's Encrypt or ZeroSSL and renew automatically. -- renewal-monitoring (REASONED; docs-rolling, letsencrypt-notice): Set the ACME email contact but monitor renewal yourself: Let's Encrypt stopped expiration emails. -- backend (REASONED; docs-rolling): reverse_proxy sends traffic to 127.0.0.1:3000; keep the app private to prevent TLS/auth bypass. -- hsts (REASONED; docs-rolling): Caddy sends no HSTS automatically; add it only after HTTPS works across every included subdomain. -- forwarded (REASONED; docs-rolling): Caddy ignores untrusted client X-Forwarded-* by default; configure trusted_proxies behind a trusted CDN/proxy. -- internal-tls (REASONED; docs-rolling): tls internal uses a local CA; clients must trust its root, with caddy trust for the local host. tls also accepts explicit cert/key files. -- basic (REASONED; docs-rolling): basic_auth uses hashed passwords from caddy hash-password; it was named basicauth before v2.8.0 and remains single-factor. -- path-auth (REASONED; docs-rolling): Protect /admin and /admin/* together: path matches are exact and multiple paths are OR'ed. -- mfa (REASONED; docs-rolling, cloudflare): forward_auth (Caddy 2.5+) can delegate human authentication to Authelia; Cloudflare Access is a proposed layer that enforces MFA when its governing Access setting (policy over application over organization) requires it. -- access-origin (REASONED; docs-rolling, cloudflare): Cloudflare Access can require MFA when configured; the guide reasons that direct origin ingress can bypass Access, so restrict it; validate the Access JWT at the origin, which a tunnel's `access` setting has `cloudflared` do. Caddy still answers directly on 443. -- body-limit (REASONED; docs-rolling): Add request_body max_size 10MB inside the existing authenticated site; replacing the site without basic_auth makes it public. -- rate-limit (REASONED; docs-rolling, ratelimit-source): The standard build has no rate_limit directive; use a compiled community caddy-ratelimit module or an upstream layer. -- admin-bind (REASONED; admin, docs-rolling): The admin API defaults to localhost:2019 as of v2.11.4; never publish it or reverse-proxy to it. -- admin-authority (REASONED; docs-rolling): Reachable unauthenticated admin clients can replace config at POST /load, edit /config/ or stop with POST /stop. -- admin-isolation (REASONED; docs-rolling): Host/Origin checks are not process isolation; local untrusted workloads require a permissioned Unix socket. -- docker-admin (REASONED; tags, images, dist, admin): The official 2.11.4 Alpine, Windows Server Core 2022/2025, and Nano Server 2022/2025 runtime tags map to fba28535 Dockerfiles that EXPOSE 2019 and run the default Caddyfile without admin; absent overrides, the endpoint stays namespace-local. -- admin-override (REASONED; admin, dist, docs-rolling): A mounted admin setting or CADDY_ADMIN changes the address; do not set 0.0.0.0:2019 to make publication work. -- namespace (REASONED; images, dist, admin, docker-network, docs-rolling): The shipped admin default is localhost:2019. On Docker Engine on Linux, bridge networking keeps loopback container-local; host or container networking shares a namespace, so treat its processes as potential admin clients. -- admin-socket (REASONED; docs-rolling): admin unix//run/caddy/admin.sock uses owner-only mode 0200 by default; append |0600 to set it explicitly. -- runtime-directory (REASONED; docs-rolling, service, systemd): The dist service at 33ae08ff runs as caddy without RuntimeDirectory; systemd v262 creates /run/caddy for RuntimeDirectory=caddy at service start and owns it as caddy. Restart for the socket switch is guide reasoning. -- reload (REASONED; docs-rolling, service): The dist service at 33ae08ff uses caddy reload --config /etc/caddy/Caddyfile --force; API reload uses the Caddyfile admin address. With admin off, guide reasoning requires systemd restart. -- signal-reload (REASONED; docs-rolling): A manual run without --resume may reload startup config via SIGUSR1; API changes or reload with a different file/adapter disable signal reloads. -- on-demand (REASONED; docs-rolling): Do not enable unrestricted on_demand TLS without an ask endpoint or host allowlist; this guide does not enable it. -- verify-config (REASONED; docs-rolling): Validate /etc/caddy/Caddyfile; no validation outcome is recorded. -- verify-redirect (REASONED; docs-rolling): HTTP should redirect to HTTPS. -- verify-auth (REASONED; docs-rolling): With site-wide auth, HTTPS without credentials returns 401. -- verify-scope (REASONED; docs-rolling): The @admin variant must return 401 for /admin and /admin/x, but an app response rather than 401 for a non-admin path. -- verify-size (REASONED; docs-rolling): Authenticated 1M reaches the app and 11M returns 413; 401/000 void the control. Auth can prevent body reads; remove request_body in isolation to attribute refusal. -- verify-backend (REASONED; docs-rolling): Read all listeners: app 3000 must be loopback-only even if proxy checks pass. -- verify-admin (REASONED; docs-rolling): TCP admin must bind only loopback; absent 2019 requires confirming admin off or a socket. Check Unix socket existence and owner restrictions separately. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Caddy 2 obtains, installs, and renews publicly trusted certificates automatically and redirects HTTP to HTTPS by default. For a new deployment with a public domain, it is the shortest correct path to HTTPS: no ACME client, no renewal timer, no redirect block. @@ -397,3 +199,227 @@ ls -l /run/caddy/admin.sock # if you bound it to a unix socket: it exis - Cloudflare Access JWT validation (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104, checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json.mdx#L15-L17 - Cloudflare Tunnel `access` setting, `cloudflared` validating the Access JWT (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104, checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/partials/cloudflare-one/tunnel/origin-parameters.mdx#L196-L215 - Docker networking documentation at 4e9a5751518ed8223a8dcde53693badddd72604f (container loopback, host namespace sharing, and the `--network container:` loopback example; the Linux bridge/host distinction above is inferred from these documented namespace semantics): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L286, https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/drivers/host.md#L11-L13, https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L302-L325 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/caddy.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- admin: Caddy admin source (v2.11.4) +- images: Caddy Docker source (fba2853501d36e8a72f946ac8cb7ff64d07e48f2) +- dist: Default Caddyfile source (33ae08ff08d168572df2956ed14fbc4949880d94) +- docs-rolling: Caddy documentation (rolling) (unknown) +- service: caddyserver/dist service unit (33ae08ff08d168572df2956ed14fbc4949880d94) +- ratelimit-source: caddy-ratelimit source (5625512f24f6f59d6f64fb3aafe5eecff0b286db) +- tags: Official image tag map (d82ca5102fa6735be29d5e1fc6ce03af77eb091e) +- docker-network: Docker networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- systemd: systemd manual (v262) +- cloudflare: Cloudflare docs (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- letsencrypt-notice: Let's Encrypt expiration-email notice (2025) + +Claims (full source mapping in the JSON block below): + +- auto-https (REASONED; docs-rolling): Caddy 2 obtains and renews public certificates and redirects HTTP to HTTPS; DNS must point here and the example requires public 80/443. +- issuers (REASONED; docs-rolling): Public certificates come from Let's Encrypt or ZeroSSL and renew automatically. +- renewal-monitoring (REASONED; docs-rolling, letsencrypt-notice): Set the ACME email contact but monitor renewal yourself: Let's Encrypt stopped expiration emails. +- backend (REASONED; docs-rolling): reverse_proxy sends traffic to 127.0.0.1:3000; keep the app private to prevent TLS/auth bypass. +- hsts (REASONED; docs-rolling): Caddy sends no HSTS automatically; add it only after HTTPS works across every included subdomain. +- forwarded (REASONED; docs-rolling): Caddy ignores untrusted client X-Forwarded-* by default; configure trusted_proxies behind a trusted CDN/proxy. +- internal-tls (REASONED; docs-rolling): tls internal uses a local CA; clients must trust its root, with caddy trust for the local host. tls also accepts explicit cert/key files. +- basic (REASONED; docs-rolling): basic_auth uses hashed passwords from caddy hash-password; it was named basicauth before v2.8.0 and remains single-factor. +- path-auth (REASONED; docs-rolling): Protect /admin and /admin/* together: path matches are exact and multiple paths are OR'ed. +- mfa (REASONED; docs-rolling, cloudflare): forward_auth (Caddy 2.5+) can delegate human authentication to Authelia; Cloudflare Access is a proposed layer that enforces MFA when its governing Access setting (policy over application over organization) requires it. +- access-origin (REASONED; docs-rolling, cloudflare): Cloudflare Access can require MFA when configured; the guide reasons that direct origin ingress can bypass Access, so restrict it; validate the Access JWT at the origin, which a tunnel's `access` setting has `cloudflared` do. Caddy still answers directly on 443. +- body-limit (REASONED; docs-rolling): Add request_body max_size 10MB inside the existing authenticated site; replacing the site without basic_auth makes it public. +- rate-limit (REASONED; docs-rolling, ratelimit-source): The standard build has no rate_limit directive; use a compiled community caddy-ratelimit module or an upstream layer. +- admin-bind (REASONED; admin, docs-rolling): The admin API defaults to localhost:2019 as of v2.11.4; never publish it or reverse-proxy to it. +- admin-authority (REASONED; docs-rolling): Reachable unauthenticated admin clients can replace config at POST /load, edit /config/ or stop with POST /stop. +- admin-isolation (REASONED; docs-rolling): Host/Origin checks are not process isolation; local untrusted workloads require a permissioned Unix socket. +- docker-admin (REASONED; tags, images, dist, admin): The official 2.11.4 Alpine, Windows Server Core 2022/2025, and Nano Server 2022/2025 runtime tags map to fba28535 Dockerfiles that EXPOSE 2019 and run the default Caddyfile without admin; absent overrides, the endpoint stays namespace-local. +- admin-override (REASONED; admin, dist, docs-rolling): A mounted admin setting or CADDY_ADMIN changes the address; do not set 0.0.0.0:2019 to make publication work. +- namespace (REASONED; images, dist, admin, docker-network, docs-rolling): The shipped admin default is localhost:2019. On Docker Engine on Linux, bridge networking keeps loopback container-local; host or container networking shares a namespace, so treat its processes as potential admin clients. +- admin-socket (REASONED; docs-rolling): admin unix//run/caddy/admin.sock uses owner-only mode 0200 by default; append |0600 to set it explicitly. +- runtime-directory (REASONED; docs-rolling, service, systemd): The dist service at 33ae08ff runs as caddy without RuntimeDirectory; systemd v262 creates /run/caddy for RuntimeDirectory=caddy at service start and owns it as caddy. Restart for the socket switch is guide reasoning. +- reload (REASONED; docs-rolling, service): The dist service at 33ae08ff uses caddy reload --config /etc/caddy/Caddyfile --force; API reload uses the Caddyfile admin address. With admin off, guide reasoning requires systemd restart. +- signal-reload (REASONED; docs-rolling): A manual run without --resume may reload startup config via SIGUSR1; API changes or reload with a different file/adapter disable signal reloads. +- on-demand (REASONED; docs-rolling): Do not enable unrestricted on_demand TLS without an ask endpoint or host allowlist; this guide does not enable it. +- verify-config (REASONED; docs-rolling): Validate /etc/caddy/Caddyfile; no validation outcome is recorded. +- verify-redirect (REASONED; docs-rolling): HTTP should redirect to HTTPS. +- verify-auth (REASONED; docs-rolling): With site-wide auth, HTTPS without credentials returns 401. +- verify-scope (REASONED; docs-rolling): The @admin variant must return 401 for /admin and /admin/x, but an app response rather than 401 for a non-admin path. +- verify-size (REASONED; docs-rolling): Authenticated 1M reaches the app and 11M returns 413; 401/000 void the control. Auth can prevent body reads; remove request_body in isolation to attribute refusal. +- verify-backend (REASONED; docs-rolling): Read all listeners: app 3000 must be loopback-only even if proxy checks pass. +- verify-admin (REASONED; docs-rolling): TCP admin must bind only loopback; absent 2019 requires confirming admin off or a socket. Check Unix socket existence and owner restrictions separately. + +````json +{ + "schema": 1, + "checked": "2026-09-27", + "documentation_checked": "2026-09", + "body_sha256": "7a5caab2cbcf90c693eab11084c5f8aebc6a3bc8e613f7ab76ca8167e3a9ea28", + "components": { + "admin": { + "name": "Caddy admin source", + "basis": "v2.11.4", + "sources": { + "s35044463fa67": "https://github.com/caddyserver/caddy/blob/v2.11.4/admin.go#L1433", + "s2bbcf239df02": "https://github.com/caddyserver/caddy/blob/v2.11.4/admin.go#L58-L67" + } + }, + "images": { + "name": "Caddy Docker source", + "basis": "fba2853501d36e8a72f946ac8cb7ff64d07e48f2", + "sources": { + "s0f0994ef9fac": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L17", + "s2dce0bbf3a70": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L59-L63", + "sd2c1b05a8088": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2022/Dockerfile#L27-L32", + "sf3e9c2d4e0cd": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2022/Dockerfile#L8", + "sc5dd3f9746fe": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2025/Dockerfile#L27-L32", + "s6c800a156eac": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2025/Dockerfile#L8", + "sc5c82704d33f": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2022/Dockerfile#L10", + "sc1cb0e1fb9eb": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2022/Dockerfile#L42-L47", + "s11528dfcc516": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2025/Dockerfile#L10", + "s70b76475a281": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2025/Dockerfile#L42-L47" + } + }, + "dist": { + "name": "Default Caddyfile source", + "basis": "33ae08ff08d168572df2956ed14fbc4949880d94", + "sources": { + "s433758a90af0": "https://github.com/caddyserver/dist/blob/33ae08ff08d168572df2956ed14fbc4949880d94/config/Caddyfile" + } + }, + "docs-rolling": { + "name": "Caddy documentation (rolling)", + "basis": "unknown", + "sources": { + "s434e7b7d7f63": "https://caddyserver.com/docs/automatic-https", + "s3923477355fb": "https://caddyserver.com/docs/api", + "sdbcd496895d6": "https://caddyserver.com/docs/caddyfile/options", + "s31af5ed45a20": "https://caddyserver.com/docs/caddyfile/directives/request_body", + "sec59a61934c5": "https://caddyserver.com/docs/caddyfile/directives", + "sbc2bd9599bd1": "https://caddyserver.com/docs/caddyfile/directives/basic_auth", + "s2e47c3139bd2": "https://caddyserver.com/docs/caddyfile/directives/tls", + "s3865a73ccda3": "https://caddyserver.com/docs/caddyfile/directives/reverse_proxy", + "s82efc39a8130": "https://caddyserver.com/docs/conventions", + "s5a6da5adf385": "https://caddyserver.com/docs/caddyfile/directives/header", + "s8df93f3e3d4c": "https://caddyserver.com/docs/caddyfile/directives/forward_auth", + "sf12ad47e81b5": "https://caddyserver.com/docs/command-line#signals", + "s38b1b78ce980": "https://caddyserver.com/docs/caddyfile/matchers" + } + }, + "service": { + "name": "caddyserver/dist service unit", + "basis": "33ae08ff08d168572df2956ed14fbc4949880d94", + "sources": { + "se70d276b03a1": "https://github.com/caddyserver/dist/blob/33ae08ff08d168572df2956ed14fbc4949880d94/init/caddy.service#L22-L32" + } + }, + "ratelimit-source": { + "name": "caddy-ratelimit source", + "basis": "5625512f24f6f59d6f64fb3aafe5eecff0b286db", + "sources": { + "s093420cd3d2f": "https://raw.githubusercontent.com/mholt/caddy-ratelimit/5625512f24f6f59d6f64fb3aafe5eecff0b286db/README.md" + } + }, + "tags": { + "name": "Official image tag map", + "basis": "d82ca5102fa6735be29d5e1fc6ce03af77eb091e", + "sources": { + "sdc8aca42f5e2": "https://github.com/docker-library/official-images/blob/d82ca5102fa6735be29d5e1fc6ce03af77eb091e/library/caddy#L7-L65" + } + }, + "docker-network": { + "name": "Docker networking documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "se1437c0f19fb": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L286", + "sfce645f8b46e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/drivers/host.md#L11-L13", + "sfd29f33e4108": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L302-L325" + } + }, + "systemd": { + "name": "systemd manual", + "basis": "v262", + "sources": { + "see108b0e0bba": "https://github.com/systemd/systemd/blob/v262/man/systemd.exec.xml#L1558-L1660" + } + }, + "cloudflare": { + "name": "Cloudflare docs", + "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "s6f6104362214": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L17-L85", + "s81e5dd28d3e5": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json.mdx#L15-L17", + "sa7c8d635bfd5": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/partials/cloudflare-one/tunnel/origin-parameters.mdx#L196-L215" + } + }, + "letsencrypt-notice": { + "name": "Let's Encrypt expiration-email notice", + "basis": "2025", + "sources": { + "sdf68595235d4": "https://letsencrypt.org/2025/01/22/ending-expiration-emails/" + } + } + }, + "claims": { + "auto-https": {"text": "Caddy 2 obtains and renews public certificates and redirects HTTP to HTTPS; DNS must point here and the example requires public 80/443.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "issuers": {"text": "Public certificates come from Let's Encrypt or ZeroSSL and renew automatically.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "renewal-monitoring": {"text": "Set the ACME email contact but monitor renewal yourself: Let's Encrypt stopped expiration emails.", "components": ["docs-rolling", "letsencrypt-notice"], "sources": ["docs-rolling:sdbcd496895d6", "letsencrypt-notice:sdf68595235d4"], "status": "REASONED"}, + "backend": {"text": "reverse_proxy sends traffic to 127.0.0.1:3000; keep the app private to prevent TLS/auth bypass.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED"}, + "hsts": {"text": "Caddy sends no HSTS automatically; add it only after HTTPS works across every included subdomain.", "components": ["docs-rolling"], "sources": ["docs-rolling:s5a6da5adf385", "docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "forwarded": {"text": "Caddy ignores untrusted client X-Forwarded-* by default; configure trusted_proxies behind a trusted CDN/proxy.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED"}, + "internal-tls": {"text": "tls internal uses a local CA; clients must trust its root, with caddy trust for the local host. tls also accepts explicit cert/key files.", "components": ["docs-rolling"], "sources": ["docs-rolling:s2e47c3139bd2", "docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "basic": {"text": "basic_auth uses hashed passwords from caddy hash-password; it was named basicauth before v2.8.0 and remains single-factor.", "components": ["docs-rolling"], "sources": ["docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, + "path-auth": {"text": "Protect /admin and /admin/* together: path matches are exact and multiple paths are OR'ed.", "components": ["docs-rolling"], "sources": ["docs-rolling:s38b1b78ce980", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, + "mfa": {"text": "forward_auth (Caddy 2.5+) can delegate human authentication to Authelia; Cloudflare Access is a proposed layer that enforces MFA when its governing Access setting (policy over application over organization) requires it.", "components": ["docs-rolling", "cloudflare"], "sources": ["docs-rolling:s8df93f3e3d4c", "cloudflare:s6f6104362214"], "status": "REASONED"}, + "access-origin": {"text": "Cloudflare Access can require MFA when configured; the guide reasons that direct origin ingress can bypass Access, so restrict it; validate the Access JWT at the origin, which a tunnel's `access` setting has `cloudflared` do. Caddy still answers directly on 443.", "components": ["docs-rolling", "cloudflare"], "sources": ["docs-rolling:s3865a73ccda3", "cloudflare:s6f6104362214", "cloudflare:s81e5dd28d3e5", "cloudflare:sa7c8d635bfd5"], "status": "REASONED"}, + "body-limit": {"text": "Add request_body max_size 10MB inside the existing authenticated site; replacing the site without basic_auth makes it public.", "components": ["docs-rolling"], "sources": ["docs-rolling:s31af5ed45a20", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, + "rate-limit": {"text": "The standard build has no rate_limit directive; use a compiled community caddy-ratelimit module or an upstream layer.", "components": ["docs-rolling", "ratelimit-source"], "sources": ["docs-rolling:sec59a61934c5", "ratelimit-source:s093420cd3d2f"], "status": "REASONED"}, + "admin-bind": {"text": "The admin API defaults to localhost:2019 as of v2.11.4; never publish it or reverse-proxy to it.", "components": ["admin", "docs-rolling"], "sources": ["admin:s35044463fa67", "admin:s2bbcf239df02", "docs-rolling:s3923477355fb"], "status": "REASONED"}, + "admin-authority": {"text": "Reachable unauthenticated admin clients can replace config at POST /load, edit /config/ or stop with POST /stop.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb"], "status": "REASONED"}, + "admin-isolation": {"text": "Host/Origin checks are not process isolation; local untrusted workloads require a permissioned Unix socket.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "docs-rolling:sdbcd496895d6"], "status": "REASONED"}, + "docker-admin": {"text": "The official 2.11.4 Alpine, Windows Server Core 2022/2025, and Nano Server 2022/2025 runtime tags map to fba28535 Dockerfiles that EXPOSE 2019 and run the default Caddyfile without admin; absent overrides, the endpoint stays namespace-local.", "components": ["tags", "images", "dist", "admin"], "sources": ["tags:sdc8aca42f5e2", "images:s0f0994ef9fac", "images:s2dce0bbf3a70", "images:sd2c1b05a8088", "images:sf3e9c2d4e0cd", "images:s6c800a156eac", "images:sc5c82704d33f", "images:sc1cb0e1fb9eb", "images:s11528dfcc516", "images:s70b76475a281", "dist:s433758a90af0", "admin:s35044463fa67", "admin:s2bbcf239df02", "images:sc5dd3f9746fe"], "status": "REASONED"}, + "admin-override": {"text": "A mounted admin setting or CADDY_ADMIN changes the address; do not set 0.0.0.0:2019 to make publication work.", "components": ["admin", "dist", "docs-rolling"], "sources": ["docs-rolling:sdbcd496895d6", "admin:s35044463fa67", "admin:s2bbcf239df02", "dist:s433758a90af0"], "status": "REASONED"}, + "namespace": {"text": "The shipped admin default is localhost:2019. On Docker Engine on Linux, bridge networking keeps loopback container-local; host or container networking shares a namespace, so treat its processes as potential admin clients.", "components": ["images", "dist", "admin", "docker-network", "docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "images:s0f0994ef9fac", "images:s2dce0bbf3a70", "images:sd2c1b05a8088", "images:sf3e9c2d4e0cd", "images:sc5dd3f9746fe", "images:s6c800a156eac", "images:sc5c82704d33f", "images:sc1cb0e1fb9eb", "images:s11528dfcc516", "images:s70b76475a281", "dist:s433758a90af0", "admin:s35044463fa67", "docker-network:se1437c0f19fb", "docker-network:sfce645f8b46e", "docker-network:sfd29f33e4108"], "status": "REASONED"}, + "admin-socket": {"text": "admin unix//run/caddy/admin.sock uses owner-only mode 0200 by default; append |0600 to set it explicitly.", "components": ["docs-rolling"], "sources": ["docs-rolling:sdbcd496895d6", "docs-rolling:s82efc39a8130"], "status": "REASONED"}, + "runtime-directory": {"text": "The dist service at 33ae08ff runs as caddy without RuntimeDirectory; systemd v262 creates /run/caddy for RuntimeDirectory=caddy at service start and owns it as caddy. Restart for the socket switch is guide reasoning.", "components": ["docs-rolling", "service", "systemd"], "sources": ["docs-rolling:sdbcd496895d6", "service:se70d276b03a1", "systemd:see108b0e0bba"], "status": "REASONED"}, + "reload": {"text": "The dist service at 33ae08ff uses caddy reload --config /etc/caddy/Caddyfile --force; API reload uses the Caddyfile admin address. With admin off, guide reasoning requires systemd restart.", "components": ["docs-rolling", "service"], "sources": ["docs-rolling:sdbcd496895d6", "docs-rolling:sf12ad47e81b5", "service:se70d276b03a1"], "status": "REASONED"}, + "signal-reload": {"text": "A manual run without --resume may reload startup config via SIGUSR1; API changes or reload with a different file/adapter disable signal reloads.", "components": ["docs-rolling"], "sources": ["docs-rolling:sf12ad47e81b5"], "status": "REASONED"}, + "on-demand": {"text": "Do not enable unrestricted on_demand TLS without an ask endpoint or host allowlist; this guide does not enable it.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "verify-config": {"text": "Validate /etc/caddy/Caddyfile; no validation outcome is recorded.", "components": ["docs-rolling"], "sources": ["docs-rolling:sf12ad47e81b5"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP should redirect to HTTPS.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "With site-wide auth, HTTPS without credentials returns 401.", "components": ["docs-rolling"], "sources": ["docs-rolling:sbc2bd9599bd1"], "status": "REASONED", "verify": [1]}, + "verify-scope": {"text": "The @admin variant must return 401 for /admin and /admin/x, but an app response rather than 401 for a non-admin path.", "components": ["docs-rolling"], "sources": ["docs-rolling:s38b1b78ce980", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED", "verify": [1]}, + "verify-size": {"text": "Authenticated 1M reaches the app and 11M returns 413; 401/000 void the control. Auth can prevent body reads; remove request_body in isolation to attribute refusal.", "components": ["docs-rolling"], "sources": ["docs-rolling:s31af5ed45a20", "docs-rolling:sbc2bd9599bd1", "docs-rolling:sec59a61934c5"], "status": "REASONED", "verify": [1]}, + "verify-backend": {"text": "Read all listeners: app 3000 must be loopback-only even if proxy checks pass.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED", "verify": [1]}, + "verify-admin": {"text": "TCP admin must bind only loopback; absent 2019 requires confirming admin off or a socket. Check Unix socket existence and owner restrictions separately.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "docs-rolling:sdbcd496895d6", "docs-rolling:s82efc39a8130"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/container-hardening.md b/plugin/skills/secure-deployment-config/references/container-hardening.md index 9a3f505cb..8fa6a1dcf 100644 --- a/plugin/skills/secure-deployment-config/references/container-hardening.md +++ b/plugin/skills/secure-deployment-config/references/container-hardening.md @@ -1,173 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-18", - "documentation_checked": "2026-09", - "body_sha256": "096aac84d8e85e741360a95b6e4fbadf6b3621a153725920785f898bb479528b", - "components": { - "docker": { - "name": "Docker documentation", - "basis": "unknown", - "sources": { - "s6ca35cbd69e7": "https://docs.docker.com/reference/dockerfile/", - "s8b7485a246e4": "https://docs.docker.com/reference/compose-file/", - "s5d6294cf605e": "https://docs.docker.com/reference/cli/docker/container/run/" - } - }, - "docker-daemon": { - "name": "Docker daemon security documentation", - "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", - "sources": { - "secebea1a69ab": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/_index.md#L84-L112", - "s79490cd2f3b2": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/install/linux-postinstall.md#L29-L39", - "s8eab393a61c6": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/rootless/_index.md#L8-L21", - "s94d57fcbfafe": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1330-L1350", - "s372ddf9da1f0": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1460-L1467", - "see4d326cd914": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1495-L1515" - } - }, - "compose-2324": { - "name": "Docker Compose source and reference", - "basis": "v2.32.4", - "sources": { - "s906b5822e9c4": "https://github.com/docker/compose/blob/v2.32.4/docs/reference/compose_exec.md#L1-L7", - "sfe4c568a9887": "https://github.com/docker/compose/blob/v2.32.4/cmd/compose/exec.go#L84-L110", - "s1bf114813e47": "https://github.com/docker/compose/blob/v2.32.4/cmd/compose/up.go#L145-L155" - } - }, - "procfs-619": { - "name": "Linux process status and capabilities manuals", - "basis": "man-pages-6.19", - "sources": { - "sa94440d59945": "https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man/man5/proc_pid_status.5?h=man-pages-6.19#n315", - "s74035196d5f7": "https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man/man7/capabilities.7?h=man-pages-6.19#n1367" - } - }, - "kubernetes-website": { - "name": "Kubernetes process namespace documentation", - "basis": "47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8", - "sources": { - "s3df8336a2aa8": "https://github.com/kubernetes/website/blob/47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8/content/en/docs/tasks/configure-pod-container/share-process-namespace.md#L115-L119" - } - }, - "kubernetes": { - "name": "Kubernetes documentation", - "basis": "unknown", - "sources": { - "sb78ea91d0308": "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/", - "s1a2d717974c7": "https://kubernetes.io/docs/concepts/security/pod-security-standards/", - "s953450b4076f": "https://kubernetes.io/docs/concepts/services-networking/network-policies/" - } - }, - "kubernetes-134": { - "name": "Kubernetes Pod Security Admission", - "basis": "1.34", - "sources": { - "s9b9aaf5067e2": "https://v1-34.docs.kubernetes.io/docs/concepts/security/pod-security-admission/", - "sa25682f200f0": "https://v1-34.docs.kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/" - } - }, - "kubectl-134": { - "name": "kubectl source", - "basis": "v1.34.0", - "sources": { - "s0906dc78482b": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L293-L310", - "s1adb8cde69c0": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L103-L194", - "sc98b92ba3ebf": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L591-L622", - "s2ffc04de1d63": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/util/override_options.go#L29-L90", - "sf5417ce59241": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/exec/exec.go#L52-L56" - } - }, - "rfc7396": { - "name": "JSON Merge Patch", - "basis": "RFC 7396", - "sources": { - "sda39f95d0890": "https://www.rfc-editor.org/rfc/rfc7396#section-2" - } - }, - "busybox-1361": { - "name": "BusyBox source", - "basis": "1_36_1", - "sources": { - "s14b89daa008d": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc.c#L30-L59", - "s34bdee292f79": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc_bloaty.c#L61-L93", - "s8a53fb647996": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc_bloaty.c#L888-L922" - } - } - }, - "claims": { - "docker-user": {"text": "Dockerfile USER sets build/runtime identity; set user and group, since no primary group uses root. Compose user overrides it; unset in both runs as root.", "components": ["docker"], "sources": ["docker:s6ca35cbd69e7", "docker:s8b7485a246e4"], "status": "REASONED"}, - "docker-rootfs": {"text": "Compose read_only makes the root filesystem read-only; provide tmpfs only for required writable paths.", "components": ["docker"], "sources": ["docker:s8b7485a246e4"], "status": "REASONED"}, - "docker-capabilities": {"text": "cap_drop: [ALL] removes capabilities; add back only specific required capabilities with cap_add.", "components": ["docker"], "sources": ["docker:s8b7485a246e4"], "status": "REASONED"}, - "docker-escalation": {"text": "no-new-privileges prevents setuid privilege gain; the guide lists bare, equals-true and colon-true spellings as equivalent.", "components": ["docker"], "sources": ["docker:s5d6294cf605e", "docker:s8b7485a246e4"], "status": "REASONED"}, - "docker-socket": {"text": "Never mount /var/run/docker.sock. In Docker's default rootful configuration, daemon control lets a user start a container that can alter the host filesystem without restriction, and docker-group membership grants root-level privileges; Rootless mode runs the daemon and containers without root privileges. Docker documents mounted-socket daemon control with a Docker client, and documents --privileged and host bind mounts; a socket-holding container starting a privileged sibling or bind-mounting host / to escape is this guide's assessment. Rootless mode narrowing the available host privilege but not making the mount safe is this guide's assessment.", "components": ["docker", "docker-daemon"], "sources": ["docker:s5d6294cf605e", "docker-daemon:secebea1a69ab", "docker-daemon:s79490cd2f3b2", "docker-daemon:s8eab393a61c6", "docker-daemon:s94d57fcbfafe", "docker-daemon:s372ddf9da1f0", "docker-daemon:see4d326cd914"], "status": "REASONED"}, - "kube-user": {"text": "runAsNonRoot rejects root; pin runAsUser and runAsGroup to 10001, with the guide noting runtime-default GID 0 if unset.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308"], "status": "REASONED"}, - "kube-rootfs": {"text": "Set readOnlyRootFilesystem per container; this is a separate recommendation, not a restricted Pod Security requirement.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, - "kube-escalation": {"text": "Set allowPrivilegeEscalation: false in the container securityContext.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, - "kube-capabilities": {"text": "Drop ALL capabilities in the container securityContext.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, - "kube-seccomp": {"text": "RuntimeDefault selects the runtime syscall filter instead of Unconfined.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, - "admission": {"text": "Namespace enforce: restricted rejects new violating Pods: privileged mode, host namespaces/ports, root, missing capability drops or Unconfined seccomp.", "components": ["kubernetes", "kubernetes-134"], "sources": ["kubernetes:s1a2d717974c7", "kubernetes-134:s9b9aaf5067e2"], "status": "REASONED"}, - "existing-pods": {"text": "Adding the namespace label warns about existing violating Pods without evicting them; recreate workloads.", "components": ["kubernetes", "kubernetes-134"], "sources": ["kubernetes:s1a2d717974c7", "kubernetes-134:sa25682f200f0"], "status": "REASONED"}, - "policy-semantics": {"text": "NetworkPolicies are additive and require an enforcing CNI; both source egress and destination ingress must allow a connection.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, - "default-deny": {"text": "The empty podSelector with Ingress and Egress isolates all pods selected in the namespace before narrow allowances.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, - "dns": {"text": "The app egress rule permits UDP/TCP 53 to all kube-system pods, not just CoreDNS; narrow it to the actual resolver.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, - "database-path": {"text": "Allow app egress to role: db and db ingress from role: app on TCP 5432; other paths governed by these namespace policies are refused.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, - "policy-limits": {"text": "hostNetwork and same-node traffic handling are implementation-dependent; database TLS/authentication remain separate controls.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, - "verify-user": {"text": "Compose exec targets the app service in the resolved project and id must report a nonzero UID; using the actual container name for bare docker exec is this guide's advice.", "components": ["docker", "compose-2324"], "sources": ["docker:s6ca35cbd69e7", "docker:s8b7485a246e4", "compose-2324:s906b5822e9c4", "compose-2324:sfe4c568a9887"], "status": "REASONED", "verify": [1]}, - "verify-rootfs": {"text": "With read_only false, touch on an existing owned path must print WRITABLE; after docker compose up -d --force-recreate with read_only true, it must print blocked. ENOENT or EACCES can also print blocked on a writable filesystem, so blocked alone is not proof of a read-only root. This guide's assessment: the errno, including typical EROFS, depends on the image and path; the probe's exit 0 does not discriminate.", "components": ["docker", "compose-2324"], "sources": ["docker:s8b7485a246e4", "compose-2324:s1bf114813e47"], "status": "REASONED", "verify": [1]}, - "verify-context": {"text": "kubectl get reads the declared context; kubectl exec inspects the application process for nonzero UID, NoNewPrivs:1, Seccomp:2 and zero effective/bounding capabilities. Clearing CapBnd under drop:[ALL] is this guide's expected runtime state; with shareProcessNamespace, PID 1 belongs to the pod sandbox, so select the application PID.", "components": ["kubernetes", "kubectl-134", "procfs-619", "kubernetes-website"], "sources": ["kubernetes:sb78ea91d0308", "kubectl-134:sf5417ce59241", "procfs-619:sa94440d59945", "procfs-619:s74035196d5f7", "kubernetes-website:s3df8336a2aa8"], "status": "REASONED", "verify": [1]}, - "verify-probe": {"text": "Resolve the db ClusterIP once; keep matching container names and restricted-compliant overrides for busybox:1.36 probes. Pinned kubectl and BusyBox sources explain merge and nc flags; the busybox:1.36 image's nc build and options remain unverified.", "components": ["kubernetes", "kubectl-134", "rfc7396", "busybox-1361"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7", "kubernetes:s953450b4076f", "kubectl-134:s0906dc78482b", "kubectl-134:s1adb8cde69c0", "kubectl-134:sc98b92ba3ebf", "kubectl-134:s2ffc04de1d63", "rfc7396:sda39f95d0890", "busybox-1361:s14b89daa008d", "busybox-1361:s34bdee292f79", "busybox-1361:s8a53fb647996"], "status": "REASONED", "verify": [1]}, - "verify-segmentation": {"text": "The role: app TCP probe must succeed first; role: other to the same IP must fail at TCP, not DNS. This tests combined segmentation, not either policy layer independently.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED", "verify": [1]} - } -} ---- # Containers: non-root, dropped capabilities, read-only root, and network segmentation - -**Version basis** +Written against Docker documentation (no recorded version), Docker Compose source and reference v2.32.4, Kubernetes Pod Security Admission 1.34, kubectl source v1.34.0, Linux process status and capabilities manuals man-pages-6.19, JSON Merge Patch RFC 7396, and BusyBox source 1_36_1; 2 further sources are pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 22 claims, all reasoned from cited sources. -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-18; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- docker: Docker documentation (unknown) -- docker-daemon: Docker daemon security documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- compose-2324: Docker Compose source and reference (v2.32.4) -- procfs-619: Linux process status and capabilities manuals (man-pages-6.19) -- kubernetes-website: Kubernetes process namespace documentation (47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8) -- kubernetes: Kubernetes documentation (unknown) -- kubernetes-134: Kubernetes Pod Security Admission (1.34) -- kubectl-134: kubectl source (v1.34.0) -- rfc7396: JSON Merge Patch (RFC 7396) -- busybox-1361: BusyBox source (1_36_1) - -Claims (full source mapping in the front matter): - -- docker-user (REASONED; docker): Dockerfile USER sets build/runtime identity; set user and group, since no primary group uses root. Compose user overrides it; unset in both runs as root. -- docker-rootfs (REASONED; docker): Compose read_only makes the root filesystem read-only; provide tmpfs only for required writable paths. -- docker-capabilities (REASONED; docker): cap_drop: [ALL] removes capabilities; add back only specific required capabilities with cap_add. -- docker-escalation (REASONED; docker): no-new-privileges prevents setuid privilege gain; the guide lists bare, equals-true and colon-true spellings as equivalent. -- docker-socket (REASONED; docker, docker-daemon): Never mount /var/run/docker.sock. In Docker's default rootful configuration, daemon control lets a user start a container that can alter the host filesystem without restriction, and docker-group membership grants root-level privileges; Rootless mode runs the daemon and containers without root privileges. Docker documents mounted-socket daemon control with a Docker client, and documents --privileged and host bind mounts; a socket-holding container starting a privileged sibling or bind-mounting host / to escape is this guide's assessment. Rootless mode narrowing the available host privilege but not making the mount safe is this guide's assessment. -- kube-user (REASONED; kubernetes): runAsNonRoot rejects root; pin runAsUser and runAsGroup to 10001, with the guide noting runtime-default GID 0 if unset. -- kube-rootfs (REASONED; kubernetes): Set readOnlyRootFilesystem per container; this is a separate recommendation, not a restricted Pod Security requirement. -- kube-escalation (REASONED; kubernetes): Set allowPrivilegeEscalation: false in the container securityContext. -- kube-capabilities (REASONED; kubernetes): Drop ALL capabilities in the container securityContext. -- kube-seccomp (REASONED; kubernetes): RuntimeDefault selects the runtime syscall filter instead of Unconfined. -- admission (REASONED; kubernetes, kubernetes-134): Namespace enforce: restricted rejects new violating Pods: privileged mode, host namespaces/ports, root, missing capability drops or Unconfined seccomp. -- existing-pods (REASONED; kubernetes, kubernetes-134): Adding the namespace label warns about existing violating Pods without evicting them; recreate workloads. -- policy-semantics (REASONED; kubernetes): NetworkPolicies are additive and require an enforcing CNI; both source egress and destination ingress must allow a connection. -- default-deny (REASONED; kubernetes): The empty podSelector with Ingress and Egress isolates all pods selected in the namespace before narrow allowances. -- dns (REASONED; kubernetes): The app egress rule permits UDP/TCP 53 to all kube-system pods, not just CoreDNS; narrow it to the actual resolver. -- database-path (REASONED; kubernetes): Allow app egress to role: db and db ingress from role: app on TCP 5432; other paths governed by these namespace policies are refused. -- policy-limits (REASONED; kubernetes): hostNetwork and same-node traffic handling are implementation-dependent; database TLS/authentication remain separate controls. -- verify-user (REASONED; docker, compose-2324): Compose exec targets the app service in the resolved project and id must report a nonzero UID; using the actual container name for bare docker exec is this guide's advice. -- verify-rootfs (REASONED; docker, compose-2324): With read_only false, touch on an existing owned path must print WRITABLE; after docker compose up -d --force-recreate with read_only true, it must print blocked. ENOENT or EACCES can also print blocked on a writable filesystem, so blocked alone is not proof of a read-only root. This guide's assessment: the errno, including typical EROFS, depends on the image and path; the probe's exit 0 does not discriminate. -- verify-context (REASONED; kubernetes, kubectl-134, procfs-619, kubernetes-website): kubectl get reads the declared context; kubectl exec inspects the application process for nonzero UID, NoNewPrivs:1, Seccomp:2 and zero effective/bounding capabilities. Clearing CapBnd under drop:[ALL] is this guide's expected runtime state; with shareProcessNamespace, PID 1 belongs to the pod sandbox, so select the application PID. -- verify-probe (REASONED; kubernetes, kubectl-134, rfc7396, busybox-1361): Resolve the db ClusterIP once; keep matching container names and restricted-compliant overrides for busybox:1.36 probes. Pinned kubectl and BusyBox sources explain merge and nc flags; the busybox:1.36 image's nc build and options remain unverified. -- verify-segmentation (REASONED; kubernetes): The role: app TCP probe must succeed first; role: other to the same IP must fail at TCP, not DNS. This tests combined segmentation, not either policy layer independently. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. A model server, chat UI, or proxy is the thing most likely to face untrusted input, so treat its container as compromised eventually and limit what that gets an attacker: not root, not the host's capabilities, not a writable filesystem, and not a route to the rest of the network. @@ -358,3 +193,194 @@ kubectl run probe-forbidden --rm -it --restart=Never --image=busybox:1.36 --labe - Linux man-pages man-pages-6.19 capability bounding set (checked October 2026): https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man/man7/capabilities.7?h=man-pages-6.19#n1367 - kubectl v1.34.0 exec Pod and container examples (checked October 2026): https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/exec/exec.go#L52-L56 - Kubernetes website 47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8 shared process namespace and pod sandbox (checked October 2026): https://github.com/kubernetes/website/blob/47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8/content/en/docs/tasks/configure-pod-container/share-process-namespace.md#L115-L119 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/container-hardening.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- docker: Docker documentation (unknown) +- docker-daemon: Docker daemon security documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- compose-2324: Docker Compose source and reference (v2.32.4) +- kubernetes-website: Kubernetes process namespace documentation (47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8) +- kubernetes: Kubernetes documentation (unknown) +- kubernetes-134: Kubernetes Pod Security Admission (1.34) +- kubectl-134: kubectl source (v1.34.0) +- procfs-619: Linux process status and capabilities manuals (man-pages-6.19) +- rfc7396: JSON Merge Patch (RFC 7396) +- busybox-1361: BusyBox source (1_36_1) + +Claims (full source mapping in the JSON block below): + +- docker-user (REASONED; docker): Dockerfile USER sets build/runtime identity; set user and group, since no primary group uses root. Compose user overrides it; unset in both runs as root. +- docker-rootfs (REASONED; docker): Compose read_only makes the root filesystem read-only; provide tmpfs only for required writable paths. +- docker-capabilities (REASONED; docker): cap_drop: [ALL] removes capabilities; add back only specific required capabilities with cap_add. +- docker-escalation (REASONED; docker): no-new-privileges prevents setuid privilege gain; the guide lists bare, equals-true and colon-true spellings as equivalent. +- docker-socket (REASONED; docker, docker-daemon): Never mount /var/run/docker.sock. In Docker's default rootful configuration, daemon control lets a user start a container that can alter the host filesystem without restriction, and docker-group membership grants root-level privileges; Rootless mode runs the daemon and containers without root privileges. Docker documents mounted-socket daemon control with a Docker client, and documents --privileged and host bind mounts; a socket-holding container starting a privileged sibling or bind-mounting host / to escape is this guide's assessment. Rootless mode narrowing the available host privilege but not making the mount safe is this guide's assessment. +- kube-user (REASONED; kubernetes): runAsNonRoot rejects root; pin runAsUser and runAsGroup to 10001, with the guide noting runtime-default GID 0 if unset. +- kube-rootfs (REASONED; kubernetes): Set readOnlyRootFilesystem per container; this is a separate recommendation, not a restricted Pod Security requirement. +- kube-escalation (REASONED; kubernetes): Set allowPrivilegeEscalation: false in the container securityContext. +- kube-capabilities (REASONED; kubernetes): Drop ALL capabilities in the container securityContext. +- kube-seccomp (REASONED; kubernetes): RuntimeDefault selects the runtime syscall filter instead of Unconfined. +- admission (REASONED; kubernetes, kubernetes-134): Namespace enforce: restricted rejects new violating Pods: privileged mode, host namespaces/ports, root, missing capability drops or Unconfined seccomp. +- existing-pods (REASONED; kubernetes, kubernetes-134): Adding the namespace label warns about existing violating Pods without evicting them; recreate workloads. +- policy-semantics (REASONED; kubernetes): NetworkPolicies are additive and require an enforcing CNI; both source egress and destination ingress must allow a connection. +- default-deny (REASONED; kubernetes): The empty podSelector with Ingress and Egress isolates all pods selected in the namespace before narrow allowances. +- dns (REASONED; kubernetes): The app egress rule permits UDP/TCP 53 to all kube-system pods, not just CoreDNS; narrow it to the actual resolver. +- database-path (REASONED; kubernetes): Allow app egress to role: db and db ingress from role: app on TCP 5432; other paths governed by these namespace policies are refused. +- policy-limits (REASONED; kubernetes): hostNetwork and same-node traffic handling are implementation-dependent; database TLS/authentication remain separate controls. +- verify-user (REASONED; docker, compose-2324): Compose exec targets the app service in the resolved project and id must report a nonzero UID; using the actual container name for bare docker exec is this guide's advice. +- verify-rootfs (REASONED; docker, compose-2324): With read_only false, touch on an existing owned path must print WRITABLE; after docker compose up -d --force-recreate with read_only true, it must print blocked. ENOENT or EACCES can also print blocked on a writable filesystem, so blocked alone is not proof of a read-only root. This guide's assessment: the errno, including typical EROFS, depends on the image and path; the probe's exit 0 does not discriminate. +- verify-context (REASONED; kubernetes, kubectl-134, procfs-619, kubernetes-website): kubectl get reads the declared context; kubectl exec inspects the application process for nonzero UID, NoNewPrivs:1, Seccomp:2 and zero effective/bounding capabilities. Clearing CapBnd under drop:[ALL] is this guide's expected runtime state; with shareProcessNamespace, PID 1 belongs to the pod sandbox, so select the application PID. +- verify-probe (REASONED; kubernetes, kubectl-134, rfc7396, busybox-1361): Resolve the db ClusterIP once; keep matching container names and restricted-compliant overrides for busybox:1.36 probes. Pinned kubectl and BusyBox sources explain merge and nc flags; the busybox:1.36 image's nc build and options remain unverified. +- verify-segmentation (REASONED; kubernetes): The role: app TCP probe must succeed first; role: other to the same IP must fail at TCP, not DNS. This tests combined segmentation, not either policy layer independently. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "096aac84d8e85e741360a95b6e4fbadf6b3621a153725920785f898bb479528b", + "components": { + "docker": { + "name": "Docker documentation", + "basis": "unknown", + "sources": { + "s6ca35cbd69e7": "https://docs.docker.com/reference/dockerfile/", + "s8b7485a246e4": "https://docs.docker.com/reference/compose-file/", + "s5d6294cf605e": "https://docs.docker.com/reference/cli/docker/container/run/" + } + }, + "docker-daemon": { + "name": "Docker daemon security documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "secebea1a69ab": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/_index.md#L84-L112", + "s79490cd2f3b2": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/install/linux-postinstall.md#L29-L39", + "s8eab393a61c6": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/rootless/_index.md#L8-L21", + "s94d57fcbfafe": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1330-L1350", + "s372ddf9da1f0": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1460-L1467", + "see4d326cd914": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1495-L1515" + } + }, + "compose-2324": { + "name": "Docker Compose source and reference", + "basis": "v2.32.4", + "sources": { + "s906b5822e9c4": "https://github.com/docker/compose/blob/v2.32.4/docs/reference/compose_exec.md#L1-L7", + "sfe4c568a9887": "https://github.com/docker/compose/blob/v2.32.4/cmd/compose/exec.go#L84-L110", + "s1bf114813e47": "https://github.com/docker/compose/blob/v2.32.4/cmd/compose/up.go#L145-L155" + } + }, + "kubernetes-website": { + "name": "Kubernetes process namespace documentation", + "basis": "47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8", + "sources": { + "s3df8336a2aa8": "https://github.com/kubernetes/website/blob/47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8/content/en/docs/tasks/configure-pod-container/share-process-namespace.md#L115-L119" + } + }, + "kubernetes": { + "name": "Kubernetes documentation", + "basis": "unknown", + "sources": { + "sb78ea91d0308": "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/", + "s1a2d717974c7": "https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "s953450b4076f": "https://kubernetes.io/docs/concepts/services-networking/network-policies/" + } + }, + "kubernetes-134": { + "name": "Kubernetes Pod Security Admission", + "basis": "1.34", + "sources": { + "s9b9aaf5067e2": "https://v1-34.docs.kubernetes.io/docs/concepts/security/pod-security-admission/", + "sa25682f200f0": "https://v1-34.docs.kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/" + } + }, + "kubectl-134": { + "name": "kubectl source", + "basis": "v1.34.0", + "sources": { + "s0906dc78482b": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L293-L310", + "s1adb8cde69c0": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L103-L194", + "sc98b92ba3ebf": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L591-L622", + "s2ffc04de1d63": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/util/override_options.go#L29-L90", + "sf5417ce59241": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/exec/exec.go#L52-L56" + } + }, + "procfs-619": { + "name": "Linux process status and capabilities manuals", + "basis": "man-pages-6.19", + "sources": { + "sa94440d59945": "https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man/man5/proc_pid_status.5?h=man-pages-6.19#n315", + "s74035196d5f7": "https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man/man7/capabilities.7?h=man-pages-6.19#n1367" + } + }, + "rfc7396": { + "name": "JSON Merge Patch", + "basis": "RFC 7396", + "sources": { + "sda39f95d0890": "https://www.rfc-editor.org/rfc/rfc7396#section-2" + } + }, + "busybox-1361": { + "name": "BusyBox source", + "basis": "1_36_1", + "sources": { + "s14b89daa008d": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc.c#L30-L59", + "s34bdee292f79": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc_bloaty.c#L61-L93", + "s8a53fb647996": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc_bloaty.c#L888-L922" + } + } + }, + "claims": { + "docker-user": {"text": "Dockerfile USER sets build/runtime identity; set user and group, since no primary group uses root. Compose user overrides it; unset in both runs as root.", "components": ["docker"], "sources": ["docker:s6ca35cbd69e7", "docker:s8b7485a246e4"], "status": "REASONED"}, + "docker-rootfs": {"text": "Compose read_only makes the root filesystem read-only; provide tmpfs only for required writable paths.", "components": ["docker"], "sources": ["docker:s8b7485a246e4"], "status": "REASONED"}, + "docker-capabilities": {"text": "cap_drop: [ALL] removes capabilities; add back only specific required capabilities with cap_add.", "components": ["docker"], "sources": ["docker:s8b7485a246e4"], "status": "REASONED"}, + "docker-escalation": {"text": "no-new-privileges prevents setuid privilege gain; the guide lists bare, equals-true and colon-true spellings as equivalent.", "components": ["docker"], "sources": ["docker:s5d6294cf605e", "docker:s8b7485a246e4"], "status": "REASONED"}, + "docker-socket": {"text": "Never mount /var/run/docker.sock. In Docker's default rootful configuration, daemon control lets a user start a container that can alter the host filesystem without restriction, and docker-group membership grants root-level privileges; Rootless mode runs the daemon and containers without root privileges. Docker documents mounted-socket daemon control with a Docker client, and documents --privileged and host bind mounts; a socket-holding container starting a privileged sibling or bind-mounting host / to escape is this guide's assessment. Rootless mode narrowing the available host privilege but not making the mount safe is this guide's assessment.", "components": ["docker", "docker-daemon"], "sources": ["docker:s5d6294cf605e", "docker-daemon:secebea1a69ab", "docker-daemon:s79490cd2f3b2", "docker-daemon:s8eab393a61c6", "docker-daemon:s94d57fcbfafe", "docker-daemon:s372ddf9da1f0", "docker-daemon:see4d326cd914"], "status": "REASONED"}, + "kube-user": {"text": "runAsNonRoot rejects root; pin runAsUser and runAsGroup to 10001, with the guide noting runtime-default GID 0 if unset.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308"], "status": "REASONED"}, + "kube-rootfs": {"text": "Set readOnlyRootFilesystem per container; this is a separate recommendation, not a restricted Pod Security requirement.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, + "kube-escalation": {"text": "Set allowPrivilegeEscalation: false in the container securityContext.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, + "kube-capabilities": {"text": "Drop ALL capabilities in the container securityContext.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, + "kube-seccomp": {"text": "RuntimeDefault selects the runtime syscall filter instead of Unconfined.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, + "admission": {"text": "Namespace enforce: restricted rejects new violating Pods: privileged mode, host namespaces/ports, root, missing capability drops or Unconfined seccomp.", "components": ["kubernetes", "kubernetes-134"], "sources": ["kubernetes:s1a2d717974c7", "kubernetes-134:s9b9aaf5067e2"], "status": "REASONED"}, + "existing-pods": {"text": "Adding the namespace label warns about existing violating Pods without evicting them; recreate workloads.", "components": ["kubernetes", "kubernetes-134"], "sources": ["kubernetes:s1a2d717974c7", "kubernetes-134:sa25682f200f0"], "status": "REASONED"}, + "policy-semantics": {"text": "NetworkPolicies are additive and require an enforcing CNI; both source egress and destination ingress must allow a connection.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "default-deny": {"text": "The empty podSelector with Ingress and Egress isolates all pods selected in the namespace before narrow allowances.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "dns": {"text": "The app egress rule permits UDP/TCP 53 to all kube-system pods, not just CoreDNS; narrow it to the actual resolver.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "database-path": {"text": "Allow app egress to role: db and db ingress from role: app on TCP 5432; other paths governed by these namespace policies are refused.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "policy-limits": {"text": "hostNetwork and same-node traffic handling are implementation-dependent; database TLS/authentication remain separate controls.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "verify-user": {"text": "Compose exec targets the app service in the resolved project and id must report a nonzero UID; using the actual container name for bare docker exec is this guide's advice.", "components": ["docker", "compose-2324"], "sources": ["docker:s6ca35cbd69e7", "docker:s8b7485a246e4", "compose-2324:s906b5822e9c4", "compose-2324:sfe4c568a9887"], "status": "REASONED", "verify": [1]}, + "verify-rootfs": {"text": "With read_only false, touch on an existing owned path must print WRITABLE; after docker compose up -d --force-recreate with read_only true, it must print blocked. ENOENT or EACCES can also print blocked on a writable filesystem, so blocked alone is not proof of a read-only root. This guide's assessment: the errno, including typical EROFS, depends on the image and path; the probe's exit 0 does not discriminate.", "components": ["docker", "compose-2324"], "sources": ["docker:s8b7485a246e4", "compose-2324:s1bf114813e47"], "status": "REASONED", "verify": [1]}, + "verify-context": {"text": "kubectl get reads the declared context; kubectl exec inspects the application process for nonzero UID, NoNewPrivs:1, Seccomp:2 and zero effective/bounding capabilities. Clearing CapBnd under drop:[ALL] is this guide's expected runtime state; with shareProcessNamespace, PID 1 belongs to the pod sandbox, so select the application PID.", "components": ["kubernetes", "kubectl-134", "procfs-619", "kubernetes-website"], "sources": ["kubernetes:sb78ea91d0308", "kubectl-134:sf5417ce59241", "procfs-619:sa94440d59945", "procfs-619:s74035196d5f7", "kubernetes-website:s3df8336a2aa8"], "status": "REASONED", "verify": [1]}, + "verify-probe": {"text": "Resolve the db ClusterIP once; keep matching container names and restricted-compliant overrides for busybox:1.36 probes. Pinned kubectl and BusyBox sources explain merge and nc flags; the busybox:1.36 image's nc build and options remain unverified.", "components": ["kubernetes", "kubectl-134", "rfc7396", "busybox-1361"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7", "kubernetes:s953450b4076f", "kubectl-134:s0906dc78482b", "kubectl-134:s1adb8cde69c0", "kubectl-134:sc98b92ba3ebf", "kubectl-134:s2ffc04de1d63", "rfc7396:sda39f95d0890", "busybox-1361:s14b89daa008d", "busybox-1361:s34bdee292f79", "busybox-1361:s8a53fb647996"], "status": "REASONED", "verify": [1]}, + "verify-segmentation": {"text": "The role: app TCP probe must succeed first; role: other to the same IP must fail at TCP, not DNS. This tests combined segmentation, not either policy layer independently.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/cors.md b/plugin/skills/secure-deployment-config/references/cors.md index b7adb2d92..c99238183 100644 --- a/plugin/skills/secure-deployment-config/references/cors.md +++ b/plugin/skills/secure-deployment-config/references/cors.md @@ -1,88 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-27", - "documentation_checked": "2026-09", - "body_sha256": "1ab4078e43ca8a5f60f429011121b3a68578a9252ac5d688dfbcd65f7b1ad6cd", - "components": { - "protocol": { - "name": "MDN CORS documentation", - "basis": "unknown", - "sources": { - "s6265cf028385": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS" - } - }, - "express": { - "name": "Express cors middleware", - "basis": "unknown", - "sources": { - "sb6448b9a443d": "https://expressjs.com/en/resources/middleware/cors/" - } - }, - "fastapi": { - "name": "FastAPI CORSMiddleware", - "basis": "unknown", - "sources": { - "s4f1cea92e7bc": "https://fastapi.tiangolo.com/tutorial/cors/" - } - }, - "fetch": { - "name": "Fetch Standard credentials", - "basis": "unknown", - "sources": { - "s028c38ebd452": "https://fetch.spec.whatwg.org/#credentials" - } - } - }, - "claims": { - "exact-origins": {"text": "Allow exact browser origins, illustrated by Access-Control-Allow-Origin: https://app.example.com; keep environment-specific lists in configuration instead of production localhost entries.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, - "wildcard-credentials": {"text": "Browsers refuse wildcard Access-Control-Allow-Origin with credentials; the guide restricts wildcard use to public, unauthenticated, read-only resources.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, - "origin-reflection": {"text": "Reflect only origins checked against an explicit allowlist; reflecting arbitrary Origin values permits hostile sites to read credentialed responses.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, - "authentication": {"text": "CORS controls browsers rather than curl or other attackers, does not expose a port and does not replace endpoint authentication.", "components": ["protocol", "express"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d"], "status": "REASONED"}, - "express-origins": {"text": "Express cors uses origin: ['https://app.example.com'] as the allowed origin list.", "components": ["express"], "sources": ["express:sb6448b9a443d"], "status": "REASONED"}, - "express-credentials": {"text": "The Express example enables credentials with credentials: true.", "components": ["express"], "sources": ["express:sb6448b9a443d"], "status": "REASONED"}, - "fastapi-origins": {"text": "FastAPI adds CORSMiddleware with allow_origins restricted to https://app.example.com.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, - "fastapi-credentials": {"text": "The FastAPI example enables allow_credentials=True.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, - "fastapi-methods": {"text": "The FastAPI example allows only GET and POST through allow_methods.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, - "fastapi-headers": {"text": "The FastAPI example allows Authorization and Content-Type through allow_headers.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, - "verify-hostile": {"text": "The exposed API echoes https://evil.example in Access-Control-Allow-Origin; the fixed API omits that hostile origin. No deployed API or browser run is recorded.", "components": ["protocol", "express", "fastapi", "fetch"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d", "fastapi:s4f1cea92e7bc", "fetch:s028c38ebd452"], "status": "REASONED", "verify": [1]}, - "verify-allowed": {"text": "The allowed-Origin probe should return the listed app origin, with Allow-Credentials only where the browser must send cookies, HTTP authentication or TLS client certificates; no actual response is recorded.", "components": ["protocol", "express", "fastapi", "fetch"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d", "fastapi:s4f1cea92e7bc", "fetch:s028c38ebd452"], "status": "REASONED", "verify": [1]}, - "credential-types": {"text": "Browser credentials include cookies, HTTP authentication and TLS client certificates; Access-Control-Allow-Credentials: true is needed only where the browser must send credentials.", "components": ["fetch", "protocol"], "sources": ["fetch:s028c38ebd452", "protocol:s6265cf028385"], "status": "REASONED"} - } -} ---- # CORS: allow your origins, not everyone's - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-09-27; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- protocol: MDN CORS documentation (unknown) -- express: Express cors middleware (unknown) -- fastapi: FastAPI CORSMiddleware (unknown) -- fetch: Fetch Standard credentials (unknown) - -Claims (full source mapping in the front matter): +Written against MDN CORS documentation (no recorded version); 3 further sources carry no recorded version. Metadata reviewed 2026-09-27; documentation checked September 2026. The guide records 13 claims, all reasoned from cited sources. -- exact-origins (REASONED; protocol): Allow exact browser origins, illustrated by Access-Control-Allow-Origin: https://app.example.com; keep environment-specific lists in configuration instead of production localhost entries. -- wildcard-credentials (REASONED; protocol): Browsers refuse wildcard Access-Control-Allow-Origin with credentials; the guide restricts wildcard use to public, unauthenticated, read-only resources. -- origin-reflection (REASONED; protocol): Reflect only origins checked against an explicit allowlist; reflecting arbitrary Origin values permits hostile sites to read credentialed responses. -- authentication (REASONED; protocol, express): CORS controls browsers rather than curl or other attackers, does not expose a port and does not replace endpoint authentication. -- express-origins (REASONED; express): Express cors uses origin: ['https://app.example.com'] as the allowed origin list. -- express-credentials (REASONED; express): The Express example enables credentials with credentials: true. -- fastapi-origins (REASONED; fastapi): FastAPI adds CORSMiddleware with allow_origins restricted to https://app.example.com. -- fastapi-credentials (REASONED; fastapi): The FastAPI example enables allow_credentials=True. -- fastapi-methods (REASONED; fastapi): The FastAPI example allows only GET and POST through allow_methods. -- fastapi-headers (REASONED; fastapi): The FastAPI example allows Authorization and Content-Type through allow_headers. -- verify-hostile (REASONED; protocol, express, fastapi, fetch): The exposed API echoes https://evil.example in Access-Control-Allow-Origin; the fixed API omits that hostile origin. No deployed API or browser run is recorded. -- verify-allowed (REASONED; protocol, express, fastapi, fetch): The allowed-Origin probe should return the listed app origin, with Allow-Credentials only where the browser must send cookies, HTTP authentication or TLS client certificates; no actual response is recorded. -- credential-types (REASONED; fetch, protocol): Browser credentials include cookies, HTTP authentication and TLS client certificates; Access-Control-Allow-Credentials: true is needed only where the browser must send credentials. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. CORS misconfiguration does not expose a port; it lets hostile websites use your users' browsers, cookies included, against your API. AI assistants reach for `Access-Control-Allow-Origin: *` the moment a browser console shows a CORS error; that is the wrong fix for any API that authenticates. @@ -137,3 +57,109 @@ Keep the origin list in configuration per environment rather than hardcoding loc - Fetch Standard, definition of credentials: https://fetch.spec.whatwg.org/#credentials - Express `cors` middleware, for the `origin` and `credentials` options used above: https://expressjs.com/en/resources/middleware/cors/ - FastAPI CORS, for `CORSMiddleware` and its parameters: https://fastapi.tiangolo.com/tutorial/cors/ + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/cors.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- protocol: MDN CORS documentation (unknown) +- fetch: Fetch Standard credentials (unknown) +- express: Express cors middleware (unknown) +- fastapi: FastAPI CORSMiddleware (unknown) + +Claims (full source mapping in the JSON block below): + +- exact-origins (REASONED; protocol): Allow exact browser origins, illustrated by Access-Control-Allow-Origin: https://app.example.com; keep environment-specific lists in configuration instead of production localhost entries. +- wildcard-credentials (REASONED; protocol): Browsers refuse wildcard Access-Control-Allow-Origin with credentials; the guide restricts wildcard use to public, unauthenticated, read-only resources. +- origin-reflection (REASONED; protocol): Reflect only origins checked against an explicit allowlist; reflecting arbitrary Origin values permits hostile sites to read credentialed responses. +- authentication (REASONED; protocol, express): CORS controls browsers rather than curl or other attackers, does not expose a port and does not replace endpoint authentication. +- express-origins (REASONED; express): Express cors uses origin: ['https://app.example.com'] as the allowed origin list. +- express-credentials (REASONED; express): The Express example enables credentials with credentials: true. +- fastapi-origins (REASONED; fastapi): FastAPI adds CORSMiddleware with allow_origins restricted to https://app.example.com. +- fastapi-credentials (REASONED; fastapi): The FastAPI example enables allow_credentials=True. +- fastapi-methods (REASONED; fastapi): The FastAPI example allows only GET and POST through allow_methods. +- fastapi-headers (REASONED; fastapi): The FastAPI example allows Authorization and Content-Type through allow_headers. +- verify-hostile (REASONED; protocol, express, fastapi, fetch): The exposed API echoes https://evil.example in Access-Control-Allow-Origin; the fixed API omits that hostile origin. No deployed API or browser run is recorded. +- verify-allowed (REASONED; protocol, express, fastapi, fetch): The allowed-Origin probe should return the listed app origin, with Allow-Credentials only where the browser must send cookies, HTTP authentication or TLS client certificates; no actual response is recorded. +- credential-types (REASONED; fetch, protocol): Browser credentials include cookies, HTTP authentication and TLS client certificates; Access-Control-Allow-Credentials: true is needed only where the browser must send credentials. + +````json +{ + "schema": 1, + "checked": "2026-09-27", + "documentation_checked": "2026-09", + "body_sha256": "1ab4078e43ca8a5f60f429011121b3a68578a9252ac5d688dfbcd65f7b1ad6cd", + "components": { + "protocol": { + "name": "MDN CORS documentation", + "basis": "unknown", + "sources": { + "s6265cf028385": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS" + } + }, + "fetch": { + "name": "Fetch Standard credentials", + "basis": "unknown", + "sources": { + "s028c38ebd452": "https://fetch.spec.whatwg.org/#credentials" + } + }, + "express": { + "name": "Express cors middleware", + "basis": "unknown", + "sources": { + "sb6448b9a443d": "https://expressjs.com/en/resources/middleware/cors/" + } + }, + "fastapi": { + "name": "FastAPI CORSMiddleware", + "basis": "unknown", + "sources": { + "s4f1cea92e7bc": "https://fastapi.tiangolo.com/tutorial/cors/" + } + } + }, + "claims": { + "exact-origins": {"text": "Allow exact browser origins, illustrated by Access-Control-Allow-Origin: https://app.example.com; keep environment-specific lists in configuration instead of production localhost entries.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, + "wildcard-credentials": {"text": "Browsers refuse wildcard Access-Control-Allow-Origin with credentials; the guide restricts wildcard use to public, unauthenticated, read-only resources.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, + "origin-reflection": {"text": "Reflect only origins checked against an explicit allowlist; reflecting arbitrary Origin values permits hostile sites to read credentialed responses.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, + "authentication": {"text": "CORS controls browsers rather than curl or other attackers, does not expose a port and does not replace endpoint authentication.", "components": ["protocol", "express"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d"], "status": "REASONED"}, + "express-origins": {"text": "Express cors uses origin: ['https://app.example.com'] as the allowed origin list.", "components": ["express"], "sources": ["express:sb6448b9a443d"], "status": "REASONED"}, + "express-credentials": {"text": "The Express example enables credentials with credentials: true.", "components": ["express"], "sources": ["express:sb6448b9a443d"], "status": "REASONED"}, + "fastapi-origins": {"text": "FastAPI adds CORSMiddleware with allow_origins restricted to https://app.example.com.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, + "fastapi-credentials": {"text": "The FastAPI example enables allow_credentials=True.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, + "fastapi-methods": {"text": "The FastAPI example allows only GET and POST through allow_methods.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, + "fastapi-headers": {"text": "The FastAPI example allows Authorization and Content-Type through allow_headers.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, + "verify-hostile": {"text": "The exposed API echoes https://evil.example in Access-Control-Allow-Origin; the fixed API omits that hostile origin. No deployed API or browser run is recorded.", "components": ["protocol", "express", "fastapi", "fetch"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d", "fastapi:s4f1cea92e7bc", "fetch:s028c38ebd452"], "status": "REASONED", "verify": [1]}, + "verify-allowed": {"text": "The allowed-Origin probe should return the listed app origin, with Allow-Credentials only where the browser must send cookies, HTTP authentication or TLS client certificates; no actual response is recorded.", "components": ["protocol", "express", "fastapi", "fetch"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d", "fastapi:s4f1cea92e7bc", "fetch:s028c38ebd452"], "status": "REASONED", "verify": [1]}, + "credential-types": {"text": "Browser credentials include cookies, HTTP authentication and TLS client certificates; Access-Control-Allow-Credentials: true is needed only where the browser must send credentials.", "components": ["fetch", "protocol"], "sources": ["fetch:s028c38ebd452", "protocol:s6265cf028385"], "status": "REASONED"} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/docker.md b/plugin/skills/secure-deployment-config/references/docker.md index d83bed956..aa92a588e 100644 --- a/plugin/skills/secure-deployment-config/references/docker.md +++ b/plugin/skills/secure-deployment-config/references/docker.md @@ -1,10 +1,276 @@ ---- -version_basis: { +# Docker and Compose: exposure, TLS, and authentication + +Written against Docker Engine minimum boundary 28.0, Moby Docker service unit v28.3.3, Docker firewalld reload fix 28.3.3, systemd unit ordering v262, Git ignore rules v2.49.0, PostgreSQL 17, Caddy v2.11.4, and nginx authentication source release-1.26.3, plus 3 more recorded bases; 7 further sources are pinned to a commit and 8 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 26 claims, all reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + +Containers are where accidental exposure happens most. Two Docker behaviours cause it: + +1. `ports: - "3000:3000"` (or `-p 3000:3000`) publishes on every host address, `0.0.0.0` and `[::]`, at the time of writing. +2. On Linux, Docker programs iptables/nftables directly, so published ports are reachable **even when UFW or firewalld says the port is blocked**. A `ufw deny 3000` rule does not protect a published container port. + +## 1. Publish nothing except the TLS proxy + +Bind anything that must be reachable from the host to loopback, and give everything else no `ports:` entry at all; containers on the same Compose network reach each other by service name without published ports. + +```yaml +services: + app: + build: . + # no ports: entry; only the proxy is published + db: + image: postgres:17 + # no ports: entry; the app reaches it at db:5432 on the internal network +``` + +Where a host-published port is genuinely needed for local access: + +```yaml + ports: + - "127.0.0.1:3000:3000" +``` + +That host-IP restriction requires Docker Engine 28.0 or later, with the firewalld-reload caveat below. Before 28.0, under the default bridge configuration, a neighbour on the same layer-2 segment could reach a port mapped to a loopback address, a remote host could reach a container on a published port despite the host-IP binding, and an unpublished container port was reachable by routing directly to the container if the host's forwarding policy allowed it; 28.0 fixed all three. Check the running Engine version (`docker version --format '{{.Server.Version}}'`, not the client's); on an older engine, treat the address in a publish string as a convenience, not a boundary, and restrict the port in the `DOCKER-USER` chain below or upstream of the host (a cloud security group or a network ACL), since a host firewall like UFW or firewalld does not reach a published port (see the top of this guide). + +On Linux, when Docker Engine runs in the host's network namespace with firewalld running, Engine 28.2.x and 28.3.0 through 28.3.2 failed to restore container-address filtering rules after a firewalld reload. A remote host with a route to the bridge network could then reach published ports at container addresses, including loopback-only publications; unpublished ports remained filtered. Where firewalld is used, run Engine 28.3.3 or later, which fixes this regression (CVE-2025-54388, GHSA-x4rx-4gw3-53p4). Rootless Mode and Docker Desktop are unaffected by this regression. + +As this guide's assessment, where a port must be published on a routable interface, because other hosts need it but the whole internet does not, the most reliable restriction is upstream of the host, in a cloud security group or a network ACL: it does not depend on Docker's firewall backend, its userland proxy, or the host's boot order. See [cloud-firewalls.md](cloud-firewalls.md) for sourced AWS security-group, Google Cloud VPC firewall and Azure NSG rules; network-ACL configuration is not directly sourced here. On the host itself, filter it in the `DOCKER-USER` iptables chain (Docker's iptables backend only; its nftables backend has no such chain), whose rules Docker evaluates before its own accept rules. Match only new connections, so the rule does not also drop the replies to connections your own containers open, which arrive on the same interface and would otherwise lose their outbound access: + +```bash +# IPv4, run as root; replace ext_if with your external interface and the subnet with your own +iptables -I DOCKER-USER -i ext_if -m conntrack --ctstate NEW ! -s 192.0.2.0/24 -j DROP +``` + +This runs after destination NAT, so an ordinary destination match sees a container's internal IP and port, not the published host IP or port; it also covers every published port arriving on that interface, so scope it to one service with conntrack's `--ctorigdstport` and `--ctdir ORIGINAL` if you need to. As this guide's reasoning, `iptables` covers IPv4 only. Cover native IPv6 forwarding with an equivalent `ip6tables` `DOCKER-USER` rule, but note a second path: publishing without a host IP on an IPv4-only bridge with `--userland-proxy=true` (the default) also makes the port answer on the host's IPv6 addresses through Docker's userland proxy (it maps IPv6 to the container's IPv4). As this guide's reasoning, that connection terminates on a host process, so it traverses the host `INPUT` chain, not `FORWARD`, and a `DOCKER-USER` rule (which lives in `FORWARD`) never sees it. Close that path by publishing to a specific address (`-p 127.0.0.1:3000:3000` or a chosen IPv4), disabling the userland proxy, or restricting the host's IPv6 `INPUT`. Probe IPv4 and IPv6 separately, each from an allowed and a disallowed source. Confirm with `sudo iptables -nvL DOCKER-USER`, watching its counters, and by probing the port from a host outside the allowed subnet, expecting a timeout, and one inside it, expecting a connection. As this guide's assessment, the rule does not survive a reboot and is not in place while the host boots; persist just the DOCKER-USER rules with a startup unit ordered after the Docker service, not a blanket `iptables-persistent` save (which, as this guide's assessment, also captures Docker's own dynamic chains and can break container networking on the next boot), and where the boot-time window matters, rely on the upstream control. + +## 2. Terminate TLS in one proxy container + +Caddy is the least configuration ([caddy.md](caddy.md)); nginx ([nginx.md](nginx.md)) and Traefik ([traefik.md](traefik.md)) work the same way. A complete pattern: + +```yaml +services: + app: + build: . + + caddy: + image: caddy:2 + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + +volumes: + caddy_data: + caddy_config: +``` + +`Caddyfile`: + +```caddyfile +app.example.com { + reverse_proxy app:3000 +} +``` + +Caddy obtains and renews the certificate automatically ([free-certificates.md](free-certificates.md) explains the ACME requirements). As this guide's recommendation, hosts with no inbound ports but a domain you can put on Cloudflare should use [cloudflare.md](cloudflare.md); run the `cloudflared` connector as a container on the app's Compose network and point it at `http://app:3000`. As this guide's recommendation, hosts with no domain at all should use [tailscale.md](tailscale.md) (Serve for private tailnet access or Funnel for public access), or [self-signed.md](self-signed.md) for internal use. Docker documents service-name lookup on the shared Compose network; the `http://app:3000` origin is this guide's inference from that lookup and [cloudflare.md](cloudflare.md)'s HTTP origin example, while the fetched Cloudflare origin parameters show an HTTPS service URL rather than this literal HTTP value. This guide does not show the tunnel-token setup; [cloudflare.md](cloudflare.md) gives the protected `--token-file` path. + +## 3. Authentication and secrets + +- As this guide's assessment, the proxy is the natural place for a first authentication gate (basic auth per the proxy guides, or Cloudflare Access); the application still needs its own login for anything multi-user ([authentication.md](authentication.md)). The proxy is also where MFA attaches for human-facing services ([mfa.md](mfa.md)): Cloudflare Access policies can require a second factor, and an Authelia portal can handle authentication behind Caddy `forward_auth`, nginx `auth_request` or Traefik `forwardAuth`. Require MFA in the governing policy or portal; as this guide's assessment, basic auth alone is single-factor. +- Pass secrets at runtime through environment files or Docker/Compose secrets. Never bake secrets into the image. Never put secrets in Dockerfile `ENV` or `ARG`: Docker documents that both are exposed in the final image; `ENV` also persists in containers from the image, and build arguments may persist in image metadata, provenance attestations and image history. As this guide's assessment, baking a key into an image ships it to every registry the image touches. `docker image history` shows the image's history, including creation commands; as this guide's assessment, the cited reference does not establish that every argument value is shown. For build-time secrets, use a temporary secret mount for the build instruction and do not copy or print the secret into the image. +- Keep `.env` in `.gitignore` (Git ignores matching untracked files, while already tracked files are unaffected), and run containers as a non-root user (`USER` in the Dockerfile); as this guide's assessment, this means a compromised app is not root in the container. As this guide's advice, non-root is the start, not the whole of workload hardening: dropping capabilities (`cap_drop`), `--security-opt no-new-privileges` (or Compose `security_opt`), a read-only root filesystem (Compose `read_only`), and never mounting the Docker socket are in [container-hardening.md](container-hardening.md). Docker documents these option names, that only trusted users should control the daemon, and that Unix permissions can limit access to its control socket; the linked guide's hardening consequences, including the mounted-socket path, remain this guide's assessment. +- Databases in containers still need their own TLS and authentication when anything outside the Compose network connects: see [postgresql.md](postgresql.md), [mysql.md](mysql.md), [mongodb.md](mongodb.md), and [redis.md](redis.md). + +## 4. Verify + +REASONED: publication, redirect, authentication and external isolation checks; no exposed/fixed run is recorded in this guide. Expectations follow the cited Docker networking sources and linked proxy guides; this read-only review has no authorized deployment or external probe hosts. For the unauthenticated API probe, expect 401 from Caddy or nginx basic auth; Caddy `forward_auth` admits auth-service 2xx, but its rejection status is not established by the fetched lines. Expect 401/403 from nginx `auth_request`; the cited Authelia nginx configuration converts its 401 to a 302 login redirect. Traefik `forwardAuth` passes through its auth service's non-2xx status, including a login redirect; Traefik `basicAuth` calls `RequireAuth`, whose response the fetched lines do not show, so 401 is this guide's expectation rather than a status established by the fetched middleware. For Cloudflare Access, this guide expects a 302 login redirect in the configuration described in [cloudflare.md](cloudflare.md); the fetched Cloudflare lines do not establish that status. A 3xx passes only when its printed location is the gate's own login portal (for Cloudflare Access, your team's `*.cloudflareaccess.com`), never the application's own login. The command fails on any 2xx; any status other than the gate's expected rejection or a 3xx to its portal is not a pass; and an authorized request must succeed separately. + +```bash +docker compose ps # project-scoped: only the proxy shows a published (0.0.0.0/[::] or a host IP) binding; review every container on the host, IPv6 included +ss -tulnp # host sockets (TCP and UDP); socket visibility varies by Docker version and config, so a socket listing alone does not prove reachability +curl -q -sI http://app.example.com/ # expect a redirect to https:// with Caddy automatic HTTPS, or after configuring nginx or Traefik to redirect +( set -o pipefail; curl -q -g -sS --noproxy '*' -o /dev/null -w 'http=%{http_code} loc=%{redirect_url}\n' https://app.example.com/api | awk '{ print; if ($1 ~ /^http=2/) { print "FAIL: unauthenticated /api returned " substr($1, 6); exit 1 } }' ) # unauthenticated: inspect the gate's rejection and portal location; then confirm an authorized request succeeds +``` + +REASONED: preceding block; publication and socket inventory, HTTPS redirect, unauthenticated API rejection and authorized API success follow the cited Docker networking sources and linked proxy guides, as do the external isolation probes this paragraph describes; no Docker runtime or external probe hosts are available, and no exposed/fixed result is recorded. Test from a second machine on a different network where possible; the UFW bypass means testing the firewall from the host itself proves nothing about published ports. The `curl` checks above exercise the proxy's redirect and auth, not the backend, so also probe each restricted host address and published port directly (for example port 3000) from an allowed and a disallowed source, IPv4 and IPv6 separately: a loopback-bound or firewalled backend must show unreachable while the proxy stays reachable. + +REASONED: this firewalld reload check follows the Impact section of GHSA-x4rx-4gw3-53p4 and the pinned v28.3.3 rule-restoration source. It has not been demonstrated here because the authoring environment has no Docker runtime, firewalld or external test host. For a loopback-published HTTP backend using default NAT bridge filtering, confirm that the backend answers locally. From a second host with a route through the Docker host to the bridge subnet, run `curl -q -g --noproxy '*' --connect-timeout 3 --max-time 5 -v http://172.17.0.2:3000/`, replacing the address and port with the actual container address and container port. Repeat with a fresh connection after running `sudo firewall-cmd --reload` on the Docker host. Without another control masking the regression, an affected host changes from blocked to connectable; a fixed host remains blocked. Any successful TCP connection, including an HTTP 401 or 403 response, demonstrates reachability. A failed probe alone does not establish that the Engine is patched: confirm the route, target and backend health. Repeat for configured IPv6 addresses and the restricted host-address publications, and confirm that the TLS proxy remains reachable. + +## Sources (checked September 2026) + +- Docker build variables (`ENV` persists in containers; build arguments may persist in metadata, provenance and history), at 4e9a5751518ed8223a8dcde53693badddd72604f: https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/build/building/variables.md#L17-L52 +- Docker build secrets (temporary mounts for build instructions), at 4e9a5751518ed8223a8dcde53693badddd72604f: https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/build/building/secrets.md#L13-L22 +- Docker image history command (image history and creation-command output), at 4e9a5751518ed8223a8dcde53693badddd72604f: https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_image_history.yaml#L1-L118 +- Git v2.49.0 ignore rules for untracked and tracked files (checked October 2026): https://github.com/git/git/blob/v2.49.0/Documentation/gitignore.adoc#L15-L18 +- Docker Dockerfile `USER` instruction, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/_vendor/github.com/moby/buildkit/frontend/dockerfile/docs/reference.md#L2448-L2463 +- Docker Compose `cap_drop` option, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L224-L233 +- Docker Compose `read_only` option, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L1892-L1894 +- Docker Compose `security_opt` and `no-new-privileges` spellings, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L2008-L2021 +- Docker run `--security-opt no-new-privileges`, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L2495-L2502 +- Docker daemon control-socket access and host filesystem control, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/_index.md#L84-L112 +- Docker iptables and ip6tables daemon options, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/firewall-iptables.md#L8-L15 +- Docker iptables DOCKER-USER and FORWARD chains, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/firewall-iptables.md#L17-L42 +- Docker IPv6-to-IPv4 userland-proxy mapping, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L60-L62 +- Docker port publishing with an explicit host address, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L33-L38 +- Docker firewall rules and warning against modifying Docker-created rules, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15 +- Moby Docker service unit, at v28.3.3 (checked October 2026): https://github.com/moby/moby/blob/v28.3.3/contrib/init/systemd/docker.service#L1-L9 +- systemd After= unit ordering, at v262 (checked October 2026): https://github.com/systemd/systemd/blob/v262/man/systemd.unit.xml#L826-L838 +- Docker packet filtering and firewalls: https://docs.docker.com/engine/network/packet-filtering-firewalls/ +- Docker with iptables, for the `DOCKER-USER` chain (processed before Docker's own rules; matches container addresses after DNAT): https://docs.docker.com/engine/network/firewall-iptables/ +- Docker Engine 28.0 release notes, for the published-port and loopback-mapping hardening: https://docs.docker.com/engine/release-notes/28/ +- Docker Engine 28.3.3 security fix: https://docs.docker.com/engine/release-notes/28/#2833 and affected-version advisory (OSV copy of the July 2025 Moby advisory): https://osv.dev/vulnerability/GHSA-x4rx-4gw3-53p4 +- Moby 28.3.3 (pinned tag v28.3.3), `reapplyPerPortIptables` restores endpoint rules after firewalld reload: https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go +- Compose networking: https://docs.docker.com/compose/how-tos/networking/ +- Docker port publishing (with no host address, "the Docker daemon publishes ports to all host addresses (0.0.0.0 and [::])"): https://docs.docker.com/engine/network/port-publishing/ +- PostgreSQL 17 connection settings and default TCP port (checked October 2026): https://www.postgresql.org/docs/17/runtime-config-connection.html +- Caddy image fba2853501d36e8a72f946ac8cb7ff64d07e48f2 data/config paths, exposed ports and Caddyfile command (checked October 2026): https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L44-L45, https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L56-L59, https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L63 +- Caddy v2.11.4 automatic certificate management and reverse_proxy syntax (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L62-L67, https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L60 +- Caddy v2.11.4 authentication handler and 401 rejection (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/caddyauth/caddyauth.go#L84-L120 +- Caddy v2.11.4 basic-auth missing-credential path (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/caddyauth/basicauth.go#L142-L163 +- Caddy v2.11.4 forward_auth accepted-response handler (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go#L174-L194 +- Caddy v2.11.4 automatic redirect setup (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L234-L240 +- Caddy v2.11.4 HTTPS redirect route (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L544-L575 +- nginx release-1.26.3 missing basic-auth credentials (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_basic_module.c#L122-L129 +- nginx release-1.26.3 basic-auth 401 challenge (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_basic_module.c#L317-L348 +- nginx release-1.26.3 auth_request status handling (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_request_module.c#L135-L182 +- Traefik v3.5.0 basicAuth failure path (checked October 2026): https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/basic_auth.go#L67-L99 +- Traefik v3.5.0 forwardAuth keeps the auth service redirect (checked October 2026): https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/forward.go#L93-L99 +- Traefik v3.5.0 forwardAuth rejection status passthrough (checked October 2026): https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/forward.go#L233-L262 +- Cloudflare Access MFA policy enforcement (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L17-L85 +- Cloudflare Access self-hosted application protection (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54 +- Authelia v4.39.28 supported proxy integration (checked October 2026): https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L24-L32 +- Authelia v4.39.28 nginx portal redirect (checked October 2026): https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L537-L541 +- AWS security-group rules (rolling reference reused from `cloud-firewalls.md`; not rechecked here): https://docs.aws.amazon.com/vpc/latest/userguide/security-group-rules.html +- Google Cloud VPC firewall rules (rolling reference reused from `cloud-firewalls.md`; not rechecked here): https://docs.cloud.google.com/firewall/docs/using-firewalls +- Azure NSG rule properties (rolling reference reused from `cloud-firewalls.md`; not rechecked here): https://learn.microsoft.com/en-us/rest/api/virtualnetwork/network-security-groups/get +- cloudflared 2026.9.3 tunnel daemon without firewall openings (checked October 2026): https://github.com/cloudflare/cloudflared/blob/2026.9.3/README.md#L1-L5 +- cloudflared 2026.9.3 Docker image (checked October 2026): https://github.com/cloudflare/cloudflared/blob/2026.9.3/README.md#L26-L33 +- cloudflared 2026.9.3 container entry point (checked October 2026): https://github.com/cloudflare/cloudflared/blob/2026.9.3/Dockerfile#L40-L42 +- Cloudflare Tunnel origin service URL example at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/partials/cloudflare-one/tunnel/origin-parameters.mdx#L31-L49 +- Cloudflare Tunnel published application domain prerequisite at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel.mdx#L27-L37 +- Docker Compose service-name lookup at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/compose/how-tos/networking.md#L13-L17 +- Tailscale Serve private tailnet entry (rolling reference reused from `tailscale.md`; not rechecked here): https://tailscale.com/kb/1242/tailscale-serve +- Tailscale Funnel public entry (rolling reference reused from `tailscale.md`; not rechecked here): https://tailscale.com/kb/1223/funnel +- OpenSSL documentation index (rolling reference reused from `self-signed.md`; not rechecked here): https://docs.openssl.org/ + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/docker.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- boundary: Docker Engine minimum boundary (28.0) +- moby-unit: Moby Docker service unit (v28.3.3) +- reload: Docker firewalld reload fix (28.3.3) +- docker-build: Docker build and image history documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- engine: Docker Engine networking (unknown) +- engine-pinned: Docker Engine networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- compose: Compose networking (unknown) +- docker-workload: Docker workload documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- reload-advisory: Docker firewalld reload advisory (unknown) +- compose-pinned: Docker Compose networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- systemd-unit: systemd unit ordering (v262) +- git-ignore: Git ignore rules (v2.49.0) +- postgresql: PostgreSQL (17) +- caddy-image: Caddy image (fba2853501d36e8a72f946ac8cb7ff64d07e48f2) +- caddy: Caddy (v2.11.4) +- nginx-auth: nginx authentication source (release-1.26.3) +- traefik-auth: Traefik authentication source (v3.5.0) +- cloudflare-access: Cloudflare Access documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- authelia-proxy: Authelia proxy integration (v4.39.28) +- aws-firewall: AWS security group rules (unknown) +- gcp-firewall: Google Cloud VPC firewall rules (unknown) +- azure-firewall: Azure network security group rules (unknown) +- cloudflared-image: cloudflared container image (2026.9.3) +- cloudflare-tunnel: Cloudflare Tunnel documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- tailscale-entry: Tailscale entry documentation (unknown) +- self-signed-entry: OpenSSL documentation (rolling) (unknown) + +Claims (full source mapping in the JSON block below): + +- publish (REASONED; engine): Omitting the host address publishes 3000:3000 on 0.0.0.0 and [::]. +- host-firewall (REASONED; engine): Docker programs firewall rules directly; UFW or firewalld blocking a host port does not establish published-port isolation. +- private-network (REASONED; compose, postgresql): Omit app/database ports; Compose peers use service names. The example uses postgres:17 at db:5432. +- loopback (REASONED; engine, boundary, reload, reload-advisory): Publish local access as 127.0.0.1:3000:3000; check the running server version. Engine 28.0 is the minimum boundary, with 28.3.3 or later required where firewalld is used. +- old-loopback (REASONED; boundary): Before 28.0, same-L2 neighbours could reach loopback publications under the default bridge configuration. +- old-host-bind (REASONED; boundary): Before 28.0, remote hosts could reach published container ports despite the host-IP binding. +- old-unpublished (REASONED; boundary): Before 28.0, direct routing could reach unpublished container ports; 28.0 fixed the stated default-bridge exposures. +- upstream (REASONED; engine, aws-firewall, gcp-firewall, azure-firewall): Use an upstream security group or network ACL for routable publications and boot-time protection. AWS security groups, Google Cloud VPC firewall rules and Azure NSGs have rolling sources; network-ACL configuration and boot-time independence remain this guide's assessment. +- docker-user (REASONED; engine): On the iptables backend, DOCKER-USER precedes Docker accepts; the nftables backend has no such chain. Match NEW traffic from outside the allowed subnet. +- dnat (REASONED; engine): DOCKER-USER sees post-DNAT container destinations; conntrack original destination port and ORIGINAL direction scope a published service. +- ipv6 (REASONED; engine, engine-pinned): As this guide's reasoning, native IPv6 forwarding needs ip6tables; Docker documents IPv6-to-IPv4 userland-proxy mapping when no host IP is given, the bridge is IPv4-only and `--userland-proxy=true` (the default), and places DOCKER-USER in FORWARD. As this guide's reasoning, the proxy path terminates on host INPUT and bypasses DOCKER-USER. +- proxy-path (REASONED; engine, engine-pinned): Close the userland-proxy path with an explicit publish address, disabled userland proxy or IPv6 INPUT restrictions; test both address families and sources. Docker documents the explicit-address option and conditional userland-proxy mapping; the INPUT restriction and probes are this guide's reasoning and advice. +- persistence (REASONED; engine, engine-pinned, moby-unit, systemd-unit): As this guide's advice, persist only DOCKER-USER rules after Docker startup, avoiding blanket dynamic-chain saves. Moby identifies docker.service and systemd documents After= ordering; as this guide's assessment, rule non-persistence, the boot-time gap, what iptables-persistent captures and its potential to break container networking have no direct source here. +- tls (REASONED; compose, caddy-image, caddy): Example caddy:2 publishes 80:80 and 443:443, mounts Caddyfile read-only and persists data/config, proxying app:3000 with automatic certificates. +- alternate-entry (REASONED; compose, cloudflared-image, cloudflare-tunnel, compose-pinned, tailscale-entry, self-signed-entry): As this guide's advice, hosts with no inbound ports and a domain they can put on Cloudflare use a cloudflared container, while hosts with no domain at all use Tailscale or internal self-signed TLS. Cloudflare documents a tunnel daemon that needs no firewall openings, a container image, an HTTPS origin service URL and a domain prerequisite; Docker documents Compose service-name lookup. The http://app:3000 origin target on a shared Compose network is this guide's inference, not shown literally by the fetched Cloudflare lines. The tunnel-token setup is not shown here; the Tailscale and OpenSSL references are rolling, and the self-signed fallback remains this guide's advice. +- auth (REASONED; compose, caddy, nginx-auth, traefik-auth, cloudflare-access, authelia-proxy): As this guide's assessment, proxy authentication and human MFA supplement application login. Caddy and nginx basic authentication reject missing credentials; nginx auth_request returns 401/403; Traefik basicAuth calls RequireAuth and forwardAuth passes through non-2xx; Cloudflare Access policies can require MFA, and Authelia can sit behind the proxy. Require MFA in the governing policy or portal; as this guide's assessment, basic auth alone is single-factor. The Caddy forward_auth rejection status, exact Traefik basicAuth status and Cloudflare Access anonymous status are not established by these fetched lines. +- secrets (REASONED; compose, docker-build): Use runtime environment files or Compose secrets, exclude .env from Git, and never put secrets in Dockerfile ENV/ARG; use temporary build secret mounts. Docker build and history behaviour is sourced; as this guide's assessment, the cited history reference does not establish that every argument value is shown; as this guide's assessment, never bake secrets into images and do not copy or print build-time secrets into the image. No direct source for the runtime delivery methods or the .env exclusion from Git is listed. +- workload (REASONED; compose, git-ignore, docker-workload): Keep .env in .gitignore, which ignores matching untracked files but not tracked files. Use a non-root Dockerfile USER; as this guide's assessment, a compromised app then is not root in the container. Drop capabilities, use no-new-privileges and a read-only root filesystem, and never mount the Docker socket. Docker sources establish the option names, that only trusted users should control the daemon, and that Unix permissions can limit access to its control socket; the linked guide's hardening consequences, including the mounted-socket path, remain this guide's assessment. +- database (REASONED; compose): Database TLS and authentication remain necessary for connections from outside the Compose network; database vendor sources are in linked guides. +- verify-publish (REASONED; compose, engine): Compose ps is project-scoped; inventory every host container and TCP/UDP socket, including IPv6. Socket visibility alone does not prove reachability. +- verify-redirect (REASONED; compose, caddy): Verify that HTTP redirects to HTTPS: Caddy v2.11.4 configures an automatic HTTPS redirect; as this guide's configuration-dependent expectation, nginx and Traefik require a configured redirect. This is an expectation, not an observed deployment result. +- verify-auth (REASONED; compose, caddy, nginx-auth, traefik-auth, authelia-proxy, cloudflare-access): Verify unauthenticated HTTPS /api rejection with printed status and redirect location; the probe itself fails on any 2xx. A 3xx passes only when the location is the gate's own login portal, never the application's own login; verify authorized success separately. Caddy basic auth and nginx auth_basic expect 401; Caddy forward_auth admits auth-service 2xx, but its rejection status is not established by the fetched lines. nginx auth_request returns 401 or 403, while the cited Authelia nginx configuration turns 401 into 302 to its portal. Traefik forwardAuth passes through a rejecting auth service's status, including a login redirect; Traefik basicAuth's 401 is this guide's expectation because the fetched code calls RequireAuth, whose response these lines do not show. Cloudflare Access's anonymous 302 login redirect is this guide's assessment per cloudflare.md, not established by the fetched Cloudflare lines. +- verify-isolation (REASONED; engine): Probe each restricted host address and publication from allowed/disallowed sources in both families; backend isolation must coexist with proxy reachability. +- firewalld-reload (REASONED; reload, reload-advisory): Engine 28.2.x and 28.3.0 through 28.3.2 fail to restore container-address filtering after firewalld reload; 28.3.3 fixes CVE-2025-54388 (GHSA-x4rx-4gw3-53p4). +- firewalld-scope (REASONED; reload, reload-advisory): The regression affects Linux Engine in the host network namespace with firewalld: routed remote hosts can reach published container ports, including loopback publications; unpublished ports stay filtered. Rootless Mode and Docker Desktop are unaffected. +- verify-firewalld-reload (REASONED; reload, reload-advisory): Confirm local backend health, then curl the container address from a second host with a bridge-subnet route before and after sudo firewall-cmd --reload. Without masking controls, affected hosts change from blocked to connectable; fixed hosts stay blocked. Any TCP connection, including HTTP 401/403, shows reachability; failure alone does not prove patching. Check route, target, IPv6, restricted host publications and continued proxy reachability. + +````json +{ "schema": 1, "checked": "2026-10-18", "documentation_checked": "2026-09", "body_sha256": "0a7bc9ec3709af315d7dab37b1fe1b6343f4b7b6425ccde1b041955813382cf5", "components": { + "boundary": { + "name": "Docker Engine minimum boundary", + "basis": "28.0", + "sources": { + "s50eb099eac95": "https://docs.docker.com/engine/release-notes/28/" + } + }, + "moby-unit": { + "name": "Moby Docker service unit", + "basis": "v28.3.3", + "sources": { + "s6aa0d2fb34cf": "https://github.com/moby/moby/blob/v28.3.3/contrib/init/systemd/docker.service#L1-L9" + } + }, + "reload": { + "name": "Docker firewalld reload fix", + "basis": "28.3.3", + "sources": { + "s567a65842b29": "https://docs.docker.com/engine/release-notes/28/#2833", + "seaf4197aafaa": "https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go" + } + }, "docker-build": { "name": "Docker build and image history documentation", "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", @@ -34,39 +300,11 @@ version_basis: { "sc244607b20e4": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15" } }, - "moby-unit": { - "name": "Moby Docker service unit", - "basis": "v28.3.3", - "sources": { - "s6aa0d2fb34cf": "https://github.com/moby/moby/blob/v28.3.3/contrib/init/systemd/docker.service#L1-L9" - } - }, - "systemd-unit": { - "name": "systemd unit ordering", - "basis": "v262", - "sources": { - "s71dce3a476cd": "https://github.com/systemd/systemd/blob/v262/man/systemd.unit.xml#L826-L838" - } - }, - "boundary": { - "name": "Docker Engine minimum boundary", - "basis": "28.0", - "sources": { - "s50eb099eac95": "https://docs.docker.com/engine/release-notes/28/" - } - }, - "compose": { - "name": "Compose networking", - "basis": "unknown", - "sources": { - "sae565a19136c": "https://docs.docker.com/compose/how-tos/networking/" - } - }, - "git-ignore": { - "name": "Git ignore rules", - "basis": "v2.49.0", + "compose": { + "name": "Compose networking", + "basis": "unknown", "sources": { - "s5f377f0fdbd8": "https://github.com/git/git/blob/v2.49.0/Documentation/gitignore.adoc#L15-L18" + "sae565a19136c": "https://docs.docker.com/compose/how-tos/networking/" } }, "docker-workload": { @@ -81,14 +319,6 @@ version_basis: { "secebea1a69ab": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/_index.md#L84-L112" } }, - "reload": { - "name": "Docker firewalld reload fix", - "basis": "28.3.3", - "sources": { - "s567a65842b29": "https://docs.docker.com/engine/release-notes/28/#2833", - "seaf4197aafaa": "https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go" - } - }, "reload-advisory": { "name": "Docker firewalld reload advisory", "basis": "unknown", @@ -96,6 +326,27 @@ version_basis: { "s424f3c4f8285": "https://osv.dev/vulnerability/GHSA-x4rx-4gw3-53p4" } }, + "compose-pinned": { + "name": "Docker Compose networking documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "seb7ab83ee20e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/compose/how-tos/networking.md#L13-L17" + } + }, + "systemd-unit": { + "name": "systemd unit ordering", + "basis": "v262", + "sources": { + "s71dce3a476cd": "https://github.com/systemd/systemd/blob/v262/man/systemd.unit.xml#L826-L838" + } + }, + "git-ignore": { + "name": "Git ignore rules", + "basis": "v2.49.0", + "sources": { + "s5f377f0fdbd8": "https://github.com/git/git/blob/v2.49.0/Documentation/gitignore.adoc#L15-L18" + } + }, "postgresql": { "name": "PostgreSQL", "basis": "17", @@ -197,13 +448,6 @@ version_basis: { "sbd8e87e6c172": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel.mdx#L27-L37" } }, - "compose-pinned": { - "name": "Docker Compose networking documentation", - "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", - "sources": { - "seb7ab83ee20e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/compose/how-tos/networking.md#L13-L17" - } - }, "tailscale-entry": { "name": "Tailscale entry documentation", "basis": "unknown", @@ -249,223 +493,5 @@ version_basis: { "verify-firewalld-reload": {"text": "Confirm local backend health, then curl the container address from a second host with a bridge-subnet route before and after sudo firewall-cmd --reload. Without masking controls, affected hosts change from blocked to connectable; fixed hosts stay blocked. Any TCP connection, including HTTP 401/403, shows reachability; failure alone does not prove patching. Check route, target, IPv6, restricted host publications and continued proxy reachability.", "components": ["reload", "reload-advisory"], "sources": ["reload-advisory:s424f3c4f8285", "reload:seaf4197aafaa"], "status": "REASONED"} } } ---- -# Docker and Compose: exposure, TLS, and authentication - - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-18; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- docker-build: Docker build and image history documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- engine: Docker Engine networking (unknown) -- engine-pinned: Docker Engine networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- moby-unit: Moby Docker service unit (v28.3.3) -- systemd-unit: systemd unit ordering (v262) -- boundary: Docker Engine minimum boundary (28.0) -- compose: Compose networking (unknown) -- git-ignore: Git ignore rules (v2.49.0) -- docker-workload: Docker workload documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- reload: Docker firewalld reload fix (28.3.3) -- reload-advisory: Docker firewalld reload advisory (unknown) -- postgresql: PostgreSQL (17) -- caddy-image: Caddy image (fba2853501d36e8a72f946ac8cb7ff64d07e48f2) -- caddy: Caddy (v2.11.4) -- nginx-auth: nginx authentication source (release-1.26.3) -- traefik-auth: Traefik authentication source (v3.5.0) -- cloudflare-access: Cloudflare Access documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- authelia-proxy: Authelia proxy integration (v4.39.28) -- aws-firewall: AWS security group rules (unknown) -- gcp-firewall: Google Cloud VPC firewall rules (unknown) -- azure-firewall: Azure network security group rules (unknown) -- cloudflared-image: cloudflared container image (2026.9.3) -- cloudflare-tunnel: Cloudflare Tunnel documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- compose-pinned: Docker Compose networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- tailscale-entry: Tailscale entry documentation (unknown) -- self-signed-entry: OpenSSL documentation (rolling) (unknown) - -Claims (full source mapping in the front matter): - -- publish (REASONED; engine): Omitting the host address publishes 3000:3000 on 0.0.0.0 and [::]. -- host-firewall (REASONED; engine): Docker programs firewall rules directly; UFW or firewalld blocking a host port does not establish published-port isolation. -- private-network (REASONED; compose, postgresql): Omit app/database ports; Compose peers use service names. The example uses postgres:17 at db:5432. -- loopback (REASONED; engine, boundary, reload, reload-advisory): Publish local access as 127.0.0.1:3000:3000; check the running server version. Engine 28.0 is the minimum boundary, with 28.3.3 or later required where firewalld is used. -- old-loopback (REASONED; boundary): Before 28.0, same-L2 neighbours could reach loopback publications under the default bridge configuration. -- old-host-bind (REASONED; boundary): Before 28.0, remote hosts could reach published container ports despite the host-IP binding. -- old-unpublished (REASONED; boundary): Before 28.0, direct routing could reach unpublished container ports; 28.0 fixed the stated default-bridge exposures. -- upstream (REASONED; engine, aws-firewall, gcp-firewall, azure-firewall): Use an upstream security group or network ACL for routable publications and boot-time protection. AWS security groups, Google Cloud VPC firewall rules and Azure NSGs have rolling sources; network-ACL configuration and boot-time independence remain this guide's assessment. -- docker-user (REASONED; engine): On the iptables backend, DOCKER-USER precedes Docker accepts; the nftables backend has no such chain. Match NEW traffic from outside the allowed subnet. -- dnat (REASONED; engine): DOCKER-USER sees post-DNAT container destinations; conntrack original destination port and ORIGINAL direction scope a published service. -- ipv6 (REASONED; engine, engine-pinned): As this guide's reasoning, native IPv6 forwarding needs ip6tables; Docker documents IPv6-to-IPv4 userland-proxy mapping when no host IP is given, the bridge is IPv4-only and `--userland-proxy=true` (the default), and places DOCKER-USER in FORWARD. As this guide's reasoning, the proxy path terminates on host INPUT and bypasses DOCKER-USER. -- proxy-path (REASONED; engine, engine-pinned): Close the userland-proxy path with an explicit publish address, disabled userland proxy or IPv6 INPUT restrictions; test both address families and sources. Docker documents the explicit-address option and conditional userland-proxy mapping; the INPUT restriction and probes are this guide's reasoning and advice. -- persistence (REASONED; engine, engine-pinned, moby-unit, systemd-unit): As this guide's advice, persist only DOCKER-USER rules after Docker startup, avoiding blanket dynamic-chain saves. Moby identifies docker.service and systemd documents After= ordering; as this guide's assessment, rule non-persistence, the boot-time gap, what iptables-persistent captures and its potential to break container networking have no direct source here. -- tls (REASONED; compose, caddy-image, caddy): Example caddy:2 publishes 80:80 and 443:443, mounts Caddyfile read-only and persists data/config, proxying app:3000 with automatic certificates. -- alternate-entry (REASONED; compose, cloudflared-image, cloudflare-tunnel, compose-pinned, tailscale-entry, self-signed-entry): As this guide's advice, hosts with no inbound ports and a domain they can put on Cloudflare use a cloudflared container, while hosts with no domain at all use Tailscale or internal self-signed TLS. Cloudflare documents a tunnel daemon that needs no firewall openings, a container image, an HTTPS origin service URL and a domain prerequisite; Docker documents Compose service-name lookup. The http://app:3000 origin target on a shared Compose network is this guide's inference, not shown literally by the fetched Cloudflare lines. The tunnel-token setup is not shown here; the Tailscale and OpenSSL references are rolling, and the self-signed fallback remains this guide's advice. -- auth (REASONED; compose, caddy, nginx-auth, traefik-auth, cloudflare-access, authelia-proxy): As this guide's assessment, proxy authentication and human MFA supplement application login. Caddy and nginx basic authentication reject missing credentials; nginx auth_request returns 401/403; Traefik basicAuth calls RequireAuth and forwardAuth passes through non-2xx; Cloudflare Access policies can require MFA, and Authelia can sit behind the proxy. Require MFA in the governing policy or portal; as this guide's assessment, basic auth alone is single-factor. The Caddy forward_auth rejection status, exact Traefik basicAuth status and Cloudflare Access anonymous status are not established by these fetched lines. -- secrets (REASONED; compose, docker-build): Use runtime environment files or Compose secrets, exclude .env from Git, and never put secrets in Dockerfile ENV/ARG; use temporary build secret mounts. Docker build and history behaviour is sourced; as this guide's assessment, the cited history reference does not establish that every argument value is shown; as this guide's assessment, never bake secrets into images and do not copy or print build-time secrets into the image. No direct source for the runtime delivery methods or the .env exclusion from Git is listed. -- workload (REASONED; compose, git-ignore, docker-workload): Keep .env in .gitignore, which ignores matching untracked files but not tracked files. Use a non-root Dockerfile USER; as this guide's assessment, a compromised app then is not root in the container. Drop capabilities, use no-new-privileges and a read-only root filesystem, and never mount the Docker socket. Docker sources establish the option names, that only trusted users should control the daemon, and that Unix permissions can limit access to its control socket; the linked guide's hardening consequences, including the mounted-socket path, remain this guide's assessment. -- database (REASONED; compose): Database TLS and authentication remain necessary for connections from outside the Compose network; database vendor sources are in linked guides. -- verify-publish (REASONED; compose, engine): Compose ps is project-scoped; inventory every host container and TCP/UDP socket, including IPv6. Socket visibility alone does not prove reachability. -- verify-redirect (REASONED; compose, caddy): Verify that HTTP redirects to HTTPS: Caddy v2.11.4 configures an automatic HTTPS redirect; as this guide's configuration-dependent expectation, nginx and Traefik require a configured redirect. This is an expectation, not an observed deployment result. -- verify-auth (REASONED; compose, caddy, nginx-auth, traefik-auth, authelia-proxy, cloudflare-access): Verify unauthenticated HTTPS /api rejection with printed status and redirect location; the probe itself fails on any 2xx. A 3xx passes only when the location is the gate's own login portal, never the application's own login; verify authorized success separately. Caddy basic auth and nginx auth_basic expect 401; Caddy forward_auth admits auth-service 2xx, but its rejection status is not established by the fetched lines. nginx auth_request returns 401 or 403, while the cited Authelia nginx configuration turns 401 into 302 to its portal. Traefik forwardAuth passes through a rejecting auth service's status, including a login redirect; Traefik basicAuth's 401 is this guide's expectation because the fetched code calls RequireAuth, whose response these lines do not show. Cloudflare Access's anonymous 302 login redirect is this guide's assessment per cloudflare.md, not established by the fetched Cloudflare lines. -- verify-isolation (REASONED; engine): Probe each restricted host address and publication from allowed/disallowed sources in both families; backend isolation must coexist with proxy reachability. -- firewalld-reload (REASONED; reload, reload-advisory): Engine 28.2.x and 28.3.0 through 28.3.2 fail to restore container-address filtering after firewalld reload; 28.3.3 fixes CVE-2025-54388 (GHSA-x4rx-4gw3-53p4). -- firewalld-scope (REASONED; reload, reload-advisory): The regression affects Linux Engine in the host network namespace with firewalld: routed remote hosts can reach published container ports, including loopback publications; unpublished ports stay filtered. Rootless Mode and Docker Desktop are unaffected. -- verify-firewalld-reload (REASONED; reload, reload-advisory): Confirm local backend health, then curl the container address from a second host with a bridge-subnet route before and after sudo firewall-cmd --reload. Without masking controls, affected hosts change from blocked to connectable; fixed hosts stay blocked. Any TCP connection, including HTTP 401/403, shows reachability; failure alone does not prove patching. Check route, target, IPv6, restricted host publications and continued proxy reachability. +```` - -Containers are where accidental exposure happens most. Two Docker behaviours cause it: - -1. `ports: - "3000:3000"` (or `-p 3000:3000`) publishes on every host address, `0.0.0.0` and `[::]`, at the time of writing. -2. On Linux, Docker programs iptables/nftables directly, so published ports are reachable **even when UFW or firewalld says the port is blocked**. A `ufw deny 3000` rule does not protect a published container port. - -## 1. Publish nothing except the TLS proxy - -Bind anything that must be reachable from the host to loopback, and give everything else no `ports:` entry at all; containers on the same Compose network reach each other by service name without published ports. - -```yaml -services: - app: - build: . - # no ports: entry; only the proxy is published - db: - image: postgres:17 - # no ports: entry; the app reaches it at db:5432 on the internal network -``` - -Where a host-published port is genuinely needed for local access: - -```yaml - ports: - - "127.0.0.1:3000:3000" -``` - -That host-IP restriction requires Docker Engine 28.0 or later, with the firewalld-reload caveat below. Before 28.0, under the default bridge configuration, a neighbour on the same layer-2 segment could reach a port mapped to a loopback address, a remote host could reach a container on a published port despite the host-IP binding, and an unpublished container port was reachable by routing directly to the container if the host's forwarding policy allowed it; 28.0 fixed all three. Check the running Engine version (`docker version --format '{{.Server.Version}}'`, not the client's); on an older engine, treat the address in a publish string as a convenience, not a boundary, and restrict the port in the `DOCKER-USER` chain below or upstream of the host (a cloud security group or a network ACL), since a host firewall like UFW or firewalld does not reach a published port (see the top of this guide). - -On Linux, when Docker Engine runs in the host's network namespace with firewalld running, Engine 28.2.x and 28.3.0 through 28.3.2 failed to restore container-address filtering rules after a firewalld reload. A remote host with a route to the bridge network could then reach published ports at container addresses, including loopback-only publications; unpublished ports remained filtered. Where firewalld is used, run Engine 28.3.3 or later, which fixes this regression (CVE-2025-54388, GHSA-x4rx-4gw3-53p4). Rootless Mode and Docker Desktop are unaffected by this regression. - -As this guide's assessment, where a port must be published on a routable interface, because other hosts need it but the whole internet does not, the most reliable restriction is upstream of the host, in a cloud security group or a network ACL: it does not depend on Docker's firewall backend, its userland proxy, or the host's boot order. See [cloud-firewalls.md](cloud-firewalls.md) for sourced AWS security-group, Google Cloud VPC firewall and Azure NSG rules; network-ACL configuration is not directly sourced here. On the host itself, filter it in the `DOCKER-USER` iptables chain (Docker's iptables backend only; its nftables backend has no such chain), whose rules Docker evaluates before its own accept rules. Match only new connections, so the rule does not also drop the replies to connections your own containers open, which arrive on the same interface and would otherwise lose their outbound access: - -```bash -# IPv4, run as root; replace ext_if with your external interface and the subnet with your own -iptables -I DOCKER-USER -i ext_if -m conntrack --ctstate NEW ! -s 192.0.2.0/24 -j DROP -``` - -This runs after destination NAT, so an ordinary destination match sees a container's internal IP and port, not the published host IP or port; it also covers every published port arriving on that interface, so scope it to one service with conntrack's `--ctorigdstport` and `--ctdir ORIGINAL` if you need to. As this guide's reasoning, `iptables` covers IPv4 only. Cover native IPv6 forwarding with an equivalent `ip6tables` `DOCKER-USER` rule, but note a second path: publishing without a host IP on an IPv4-only bridge with `--userland-proxy=true` (the default) also makes the port answer on the host's IPv6 addresses through Docker's userland proxy (it maps IPv6 to the container's IPv4). As this guide's reasoning, that connection terminates on a host process, so it traverses the host `INPUT` chain, not `FORWARD`, and a `DOCKER-USER` rule (which lives in `FORWARD`) never sees it. Close that path by publishing to a specific address (`-p 127.0.0.1:3000:3000` or a chosen IPv4), disabling the userland proxy, or restricting the host's IPv6 `INPUT`. Probe IPv4 and IPv6 separately, each from an allowed and a disallowed source. Confirm with `sudo iptables -nvL DOCKER-USER`, watching its counters, and by probing the port from a host outside the allowed subnet, expecting a timeout, and one inside it, expecting a connection. As this guide's assessment, the rule does not survive a reboot and is not in place while the host boots; persist just the DOCKER-USER rules with a startup unit ordered after the Docker service, not a blanket `iptables-persistent` save (which, as this guide's assessment, also captures Docker's own dynamic chains and can break container networking on the next boot), and where the boot-time window matters, rely on the upstream control. - -## 2. Terminate TLS in one proxy container - -Caddy is the least configuration ([caddy.md](caddy.md)); nginx ([nginx.md](nginx.md)) and Traefik ([traefik.md](traefik.md)) work the same way. A complete pattern: - -```yaml -services: - app: - build: . - - caddy: - image: caddy:2 - ports: - - "80:80" - - "443:443" - volumes: - - ./Caddyfile:/etc/caddy/Caddyfile:ro - - caddy_data:/data - - caddy_config:/config - -volumes: - caddy_data: - caddy_config: -``` - -`Caddyfile`: - -```caddyfile -app.example.com { - reverse_proxy app:3000 -} -``` - -Caddy obtains and renews the certificate automatically ([free-certificates.md](free-certificates.md) explains the ACME requirements). As this guide's recommendation, hosts with no inbound ports but a domain you can put on Cloudflare should use [cloudflare.md](cloudflare.md); run the `cloudflared` connector as a container on the app's Compose network and point it at `http://app:3000`. As this guide's recommendation, hosts with no domain at all should use [tailscale.md](tailscale.md) (Serve for private tailnet access or Funnel for public access), or [self-signed.md](self-signed.md) for internal use. Docker documents service-name lookup on the shared Compose network; the `http://app:3000` origin is this guide's inference from that lookup and [cloudflare.md](cloudflare.md)'s HTTP origin example, while the fetched Cloudflare origin parameters show an HTTPS service URL rather than this literal HTTP value. This guide does not show the tunnel-token setup; [cloudflare.md](cloudflare.md) gives the protected `--token-file` path. - -## 3. Authentication and secrets - -- As this guide's assessment, the proxy is the natural place for a first authentication gate (basic auth per the proxy guides, or Cloudflare Access); the application still needs its own login for anything multi-user ([authentication.md](authentication.md)). The proxy is also where MFA attaches for human-facing services ([mfa.md](mfa.md)): Cloudflare Access policies can require a second factor, and an Authelia portal can handle authentication behind Caddy `forward_auth`, nginx `auth_request` or Traefik `forwardAuth`. Require MFA in the governing policy or portal; as this guide's assessment, basic auth alone is single-factor. -- Pass secrets at runtime through environment files or Docker/Compose secrets. Never bake secrets into the image. Never put secrets in Dockerfile `ENV` or `ARG`: Docker documents that both are exposed in the final image; `ENV` also persists in containers from the image, and build arguments may persist in image metadata, provenance attestations and image history. As this guide's assessment, baking a key into an image ships it to every registry the image touches. `docker image history` shows the image's history, including creation commands; as this guide's assessment, the cited reference does not establish that every argument value is shown. For build-time secrets, use a temporary secret mount for the build instruction and do not copy or print the secret into the image. -- Keep `.env` in `.gitignore` (Git ignores matching untracked files, while already tracked files are unaffected), and run containers as a non-root user (`USER` in the Dockerfile); as this guide's assessment, this means a compromised app is not root in the container. As this guide's advice, non-root is the start, not the whole of workload hardening: dropping capabilities (`cap_drop`), `--security-opt no-new-privileges` (or Compose `security_opt`), a read-only root filesystem (Compose `read_only`), and never mounting the Docker socket are in [container-hardening.md](container-hardening.md). Docker documents these option names, that only trusted users should control the daemon, and that Unix permissions can limit access to its control socket; the linked guide's hardening consequences, including the mounted-socket path, remain this guide's assessment. -- Databases in containers still need their own TLS and authentication when anything outside the Compose network connects: see [postgresql.md](postgresql.md), [mysql.md](mysql.md), [mongodb.md](mongodb.md), and [redis.md](redis.md). - -## 4. Verify - -REASONED: publication, redirect, authentication and external isolation checks; no exposed/fixed run is recorded in this guide. Expectations follow the cited Docker networking sources and linked proxy guides; this read-only review has no authorized deployment or external probe hosts. For the unauthenticated API probe, expect 401 from Caddy or nginx basic auth; Caddy `forward_auth` admits auth-service 2xx, but its rejection status is not established by the fetched lines. Expect 401/403 from nginx `auth_request`; the cited Authelia nginx configuration converts its 401 to a 302 login redirect. Traefik `forwardAuth` passes through its auth service's non-2xx status, including a login redirect; Traefik `basicAuth` calls `RequireAuth`, whose response the fetched lines do not show, so 401 is this guide's expectation rather than a status established by the fetched middleware. For Cloudflare Access, this guide expects a 302 login redirect in the configuration described in [cloudflare.md](cloudflare.md); the fetched Cloudflare lines do not establish that status. A 3xx passes only when its printed location is the gate's own login portal (for Cloudflare Access, your team's `*.cloudflareaccess.com`), never the application's own login. The command fails on any 2xx; any status other than the gate's expected rejection or a 3xx to its portal is not a pass; and an authorized request must succeed separately. - -```bash -docker compose ps # project-scoped: only the proxy shows a published (0.0.0.0/[::] or a host IP) binding; review every container on the host, IPv6 included -ss -tulnp # host sockets (TCP and UDP); socket visibility varies by Docker version and config, so a socket listing alone does not prove reachability -curl -q -sI http://app.example.com/ # expect a redirect to https:// with Caddy automatic HTTPS, or after configuring nginx or Traefik to redirect -( set -o pipefail; curl -q -g -sS --noproxy '*' -o /dev/null -w 'http=%{http_code} loc=%{redirect_url}\n' https://app.example.com/api | awk '{ print; if ($1 ~ /^http=2/) { print "FAIL: unauthenticated /api returned " substr($1, 6); exit 1 } }' ) # unauthenticated: inspect the gate's rejection and portal location; then confirm an authorized request succeeds -``` - -REASONED: preceding block; publication and socket inventory, HTTPS redirect, unauthenticated API rejection and authorized API success follow the cited Docker networking sources and linked proxy guides, as do the external isolation probes this paragraph describes; no Docker runtime or external probe hosts are available, and no exposed/fixed result is recorded. Test from a second machine on a different network where possible; the UFW bypass means testing the firewall from the host itself proves nothing about published ports. The `curl` checks above exercise the proxy's redirect and auth, not the backend, so also probe each restricted host address and published port directly (for example port 3000) from an allowed and a disallowed source, IPv4 and IPv6 separately: a loopback-bound or firewalled backend must show unreachable while the proxy stays reachable. - -REASONED: this firewalld reload check follows the Impact section of GHSA-x4rx-4gw3-53p4 and the pinned v28.3.3 rule-restoration source. It has not been demonstrated here because the authoring environment has no Docker runtime, firewalld or external test host. For a loopback-published HTTP backend using default NAT bridge filtering, confirm that the backend answers locally. From a second host with a route through the Docker host to the bridge subnet, run `curl -q -g --noproxy '*' --connect-timeout 3 --max-time 5 -v http://172.17.0.2:3000/`, replacing the address and port with the actual container address and container port. Repeat with a fresh connection after running `sudo firewall-cmd --reload` on the Docker host. Without another control masking the regression, an affected host changes from blocked to connectable; a fixed host remains blocked. Any successful TCP connection, including an HTTP 401 or 403 response, demonstrates reachability. A failed probe alone does not establish that the Engine is patched: confirm the route, target and backend health. Repeat for configured IPv6 addresses and the restricted host-address publications, and confirm that the TLS proxy remains reachable. - -## Sources (checked September 2026) - -- Docker build variables (`ENV` persists in containers; build arguments may persist in metadata, provenance and history), at 4e9a5751518ed8223a8dcde53693badddd72604f: https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/build/building/variables.md#L17-L52 -- Docker build secrets (temporary mounts for build instructions), at 4e9a5751518ed8223a8dcde53693badddd72604f: https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/build/building/secrets.md#L13-L22 -- Docker image history command (image history and creation-command output), at 4e9a5751518ed8223a8dcde53693badddd72604f: https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_image_history.yaml#L1-L118 -- Git v2.49.0 ignore rules for untracked and tracked files (checked October 2026): https://github.com/git/git/blob/v2.49.0/Documentation/gitignore.adoc#L15-L18 -- Docker Dockerfile `USER` instruction, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/_vendor/github.com/moby/buildkit/frontend/dockerfile/docs/reference.md#L2448-L2463 -- Docker Compose `cap_drop` option, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L224-L233 -- Docker Compose `read_only` option, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L1892-L1894 -- Docker Compose `security_opt` and `no-new-privileges` spellings, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L2008-L2021 -- Docker run `--security-opt no-new-privileges`, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L2495-L2502 -- Docker daemon control-socket access and host filesystem control, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/_index.md#L84-L112 -- Docker iptables and ip6tables daemon options, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/firewall-iptables.md#L8-L15 -- Docker iptables DOCKER-USER and FORWARD chains, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/firewall-iptables.md#L17-L42 -- Docker IPv6-to-IPv4 userland-proxy mapping, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L60-L62 -- Docker port publishing with an explicit host address, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L33-L38 -- Docker firewall rules and warning against modifying Docker-created rules, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15 -- Moby Docker service unit, at v28.3.3 (checked October 2026): https://github.com/moby/moby/blob/v28.3.3/contrib/init/systemd/docker.service#L1-L9 -- systemd After= unit ordering, at v262 (checked October 2026): https://github.com/systemd/systemd/blob/v262/man/systemd.unit.xml#L826-L838 -- Docker packet filtering and firewalls: https://docs.docker.com/engine/network/packet-filtering-firewalls/ -- Docker with iptables, for the `DOCKER-USER` chain (processed before Docker's own rules; matches container addresses after DNAT): https://docs.docker.com/engine/network/firewall-iptables/ -- Docker Engine 28.0 release notes, for the published-port and loopback-mapping hardening: https://docs.docker.com/engine/release-notes/28/ -- Docker Engine 28.3.3 security fix: https://docs.docker.com/engine/release-notes/28/#2833 and affected-version advisory (OSV copy of the July 2025 Moby advisory): https://osv.dev/vulnerability/GHSA-x4rx-4gw3-53p4 -- Moby 28.3.3 (pinned tag v28.3.3), `reapplyPerPortIptables` restores endpoint rules after firewalld reload: https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go -- Compose networking: https://docs.docker.com/compose/how-tos/networking/ -- Docker port publishing (with no host address, "the Docker daemon publishes ports to all host addresses (0.0.0.0 and [::])"): https://docs.docker.com/engine/network/port-publishing/ -- PostgreSQL 17 connection settings and default TCP port (checked October 2026): https://www.postgresql.org/docs/17/runtime-config-connection.html -- Caddy image fba2853501d36e8a72f946ac8cb7ff64d07e48f2 data/config paths, exposed ports and Caddyfile command (checked October 2026): https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L44-L45, https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L56-L59, https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L63 -- Caddy v2.11.4 automatic certificate management and reverse_proxy syntax (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L62-L67, https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L60 -- Caddy v2.11.4 authentication handler and 401 rejection (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/caddyauth/caddyauth.go#L84-L120 -- Caddy v2.11.4 basic-auth missing-credential path (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/caddyauth/basicauth.go#L142-L163 -- Caddy v2.11.4 forward_auth accepted-response handler (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go#L174-L194 -- Caddy v2.11.4 automatic redirect setup (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L234-L240 -- Caddy v2.11.4 HTTPS redirect route (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L544-L575 -- nginx release-1.26.3 missing basic-auth credentials (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_basic_module.c#L122-L129 -- nginx release-1.26.3 basic-auth 401 challenge (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_basic_module.c#L317-L348 -- nginx release-1.26.3 auth_request status handling (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_request_module.c#L135-L182 -- Traefik v3.5.0 basicAuth failure path (checked October 2026): https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/basic_auth.go#L67-L99 -- Traefik v3.5.0 forwardAuth keeps the auth service redirect (checked October 2026): https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/forward.go#L93-L99 -- Traefik v3.5.0 forwardAuth rejection status passthrough (checked October 2026): https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/forward.go#L233-L262 -- Cloudflare Access MFA policy enforcement (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L17-L85 -- Cloudflare Access self-hosted application protection (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54 -- Authelia v4.39.28 supported proxy integration (checked October 2026): https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L24-L32 -- Authelia v4.39.28 nginx portal redirect (checked October 2026): https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L537-L541 -- AWS security-group rules (rolling reference reused from `cloud-firewalls.md`; not rechecked here): https://docs.aws.amazon.com/vpc/latest/userguide/security-group-rules.html -- Google Cloud VPC firewall rules (rolling reference reused from `cloud-firewalls.md`; not rechecked here): https://docs.cloud.google.com/firewall/docs/using-firewalls -- Azure NSG rule properties (rolling reference reused from `cloud-firewalls.md`; not rechecked here): https://learn.microsoft.com/en-us/rest/api/virtualnetwork/network-security-groups/get -- cloudflared 2026.9.3 tunnel daemon without firewall openings (checked October 2026): https://github.com/cloudflare/cloudflared/blob/2026.9.3/README.md#L1-L5 -- cloudflared 2026.9.3 Docker image (checked October 2026): https://github.com/cloudflare/cloudflared/blob/2026.9.3/README.md#L26-L33 -- cloudflared 2026.9.3 container entry point (checked October 2026): https://github.com/cloudflare/cloudflared/blob/2026.9.3/Dockerfile#L40-L42 -- Cloudflare Tunnel origin service URL example at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/partials/cloudflare-one/tunnel/origin-parameters.mdx#L31-L49 -- Cloudflare Tunnel published application domain prerequisite at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel.mdx#L27-L37 -- Docker Compose service-name lookup at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/compose/how-tos/networking.md#L13-L17 -- Tailscale Serve private tailnet entry (rolling reference reused from `tailscale.md`; not rechecked here): https://tailscale.com/kb/1242/tailscale-serve -- Tailscale Funnel public entry (rolling reference reused from `tailscale.md`; not rechecked here): https://tailscale.com/kb/1223/funnel -- OpenSSL documentation index (rolling reference reused from `self-signed.md`; not rechecked here): https://docs.openssl.org/ diff --git a/plugin/skills/secure-deployment-config/references/firebase-supabase.md b/plugin/skills/secure-deployment-config/references/firebase-supabase.md index 89bd0b9fe..46d251875 100644 --- a/plugin/skills/secure-deployment-config/references/firebase-supabase.md +++ b/plugin/skills/secure-deployment-config/references/firebase-supabase.md @@ -1,308 +1,25 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-19", - "documentation_checked": "2026-09", - "body_sha256": "6690687db9e1d66f3e67a898fc0b030022e55be24166bc29b8c35554e8081891", - "components": { - "firebase": { - "name": "Firebase documentation", - "basis": "unknown", - "sources": { - "s100c2cdbdd59": "https://firebase.google.com/docs/rules", - "s53bfe8b21868": "https://firebase.google.com/docs/rules/basics", - "s622d3bb0665b": "https://firebase.google.com/docs/firestore/quickstart", - "sb2c933c841ef": "https://firebase.google.com/docs/database/web/start", - "s75f018e0fac7": "https://firebase.google.com/support/releases", - "s7bd11c22c7ce": "https://firebase.google.com/docs/firestore/security/rules-structure", - "sa6b1c4933e6a": "https://firebase.google.com/docs/database/security/core-syntax", - "s0a7f54463abd": "https://firebase.google.com/docs/storage/security/core-syntax", - "sd6071548f6fe": "https://firebase.google.com/docs/firestore/security/get-started", - "s14872143a9e8": "https://firebase.google.com/docs/storage/security/get-started", - "sc8f214d059d2": "https://firebase.google.com/docs/functions/callable", - "s79da8f65f90a": "https://firebase.google.com/docs/functions/callable-reference", - "s9a86df9623a6": "https://firebase.google.com/docs/functions/1st-gen/callable-1st", - "s49f78d054708": "https://firebase.google.com/docs/auth/admin/verify-id-tokens", - "s1bb711c9afeb": "https://firebase.google.com/docs/functions/http-events", - "sc9f86ea271f6": "https://firebase.google.com/docs/reference/functions/2nd-gen/node/firebase-functions.https.httpsoptions", - "sb0ff8f1726f7": "https://firebase.google.com/docs/app-check/cloud-functions", - "sa71e958c99df": "https://firebase.google.com/docs/app-check/web/recaptcha-enterprise-provider", - "sba7129c83405": "https://firebase.google.com/docs/app-check/enable-enforcement", - "safeed4cd2b67": "https://firebase.google.com/docs/projects/api-keys", - "sc78d5eb59b65": "https://docs.cloud.google.com/api-keys/docs/add-restrictions-api-keys", - "sb0d26775f3b4": "https://firebase.google.com/docs/web/learn-more", - "s77e2cbd8a29a": "https://firebase.google.com/docs/auth/web/anonymous-auth", - "s06808e4b6965": "https://docs.cloud.google.com/identity-platform/docs/admin/email-enumeration-protection", - "sd8a1bda29f5c": "https://firebase.google.com/docs/emulator-suite/install_and_configure", - "s69d9721c7f29": "https://firebase.google.com/docs/emulator-suite/use_hosting", - "s8c90f9f6c7fa": "https://firebase.google.com/docs/reference/js/auth", - "s8c8336ec252e": "https://firebase.google.com/docs/reference/js/auth.additionaluserinfo", - "s142e56472093": "https://firebase.google.com/docs/auth/admin/manage-sessions", - "s9b7e57b472c7": "https://firebase.google.com/docs/storage/admin/start", - "sa0584b797732": "https://firebase.google.com/docs/reference/kotlin/com/google/firebase/storage/StorageReference", - "sfd1e7412abf6": "https://firebase.google.com/docs/storage/web/download-files" - } - }, - "supabase": { - "name": "Supabase documentation", - "basis": "unknown", - "sources": { - "s73d939b4644e": "https://supabase.com/docs/guides/api/api-keys", - "s46cff2590323": "https://supabase.com/docs/guides/database/postgres/row-level-security", - "s741468ff35a9": "https://supabase.com/docs/guides/auth/auth-mfa", - "s6d5cf2d4dbf3": "https://supabase.com/docs/guides/api/securing-your-api", - "s20ef046ac646": "https://supabase.com/docs/guides/api/using-custom-schemas", - "sa946354db383": "https://supabase.com/docs/guides/getting-started/migrating-to-new-api-keys", - "sfc2f9ae0232b": "https://supabase.com/docs/guides/auth/signing-keys", - "s8405a9fb4b74": "https://supabase.com/docs/guides/database/functions", - "s13da01bbbba5": "https://supabase.com/docs/guides/database/extensions/http", - "s76740bffcb32": "https://supabase.com/docs/guides/database/extensions/pg_net", - "s47bb8fa3e14e": "https://supabase.com/docs/guides/database/vault", - "s1567fb838f20": "https://supabase.com/docs/guides/storage/buckets/fundamentals", - "sdba7d7b6dcf8": "https://supabase.com/docs/guides/storage/security/access-control", - "s9e1484f46eff": "https://supabase.com/docs/guides/storage/schema/helper-functions", - "sc29febbb230e": "https://supabase.com/docs/guides/storage/serving/downloads", - "see2d3be99ebd": "https://supabase.com/docs/guides/storage/uploads/standard-uploads", - "s5674b1057e14": "https://supabase.com/docs/guides/realtime/settings", - "s84efad0ba39d": "https://supabase.com/docs/guides/realtime/authorization", - "s694eab64f2ec": "https://supabase.com/docs/guides/realtime/postgres-changes", - "sc797f0b63472": "https://supabase.com/docs/guides/auth/general-configuration", - "s3a5e39fe7b4d": "https://supabase.com/docs/guides/local-development/cli/config", - "sa2dd4637969e": "https://supabase.com/docs/guides/auth/passwords", - "sad79d4d1b6ee": "https://supabase.com/docs/guides/auth/auth-anonymous", - "s182755725d74": "https://supabase.com/docs/guides/auth/auth-mfa/totp", - "s92b405dc3c7e": "https://supabase.com/docs/reference/javascript/select", - "s087c8cf42ab0": "https://supabase.com/docs/reference/javascript/rpc", - "sf3246532dd18": "https://supabase.com/docs/guides/functions", - "s7dfbb47b5d20": "https://supabase.com/docs/reference/cli/supabase-functions-deploy", - "sf7d438b8c080": "https://supabase.com/docs/guides/functions/auth", - "se52bbd097033": "https://supabase.com/docs/guides/functions/auth-headers", - "sb3c4c0993e14": "https://supabase.com/docs/guides/functions/secrets", - "sb787b5d63633": "https://supabase.com/docs/guides/platform/network-restrictions", - "s83e15676e1ff": "https://supabase.com/docs/guides/platform/ssl-enforcement", - "sf69cafb4bc6c": "https://supabase.com/docs/guides/database/postgres/roles" - } - }, - "postgres": { - "name": "PostgreSQL documentation", - "basis": "unknown", - "sources": { - "s13c54ab9cc6b": "https://www.postgresql.org/docs/current/sql-createview.html", - "saa99ef28f60f": "https://www.postgresql.org/docs/current/sql-alterview.html", - "sb6224f4ac866": "https://www.postgresql.org/docs/current/sql-revoke.html", - "se9846041ce1f": "https://www.postgresql.org/docs/current/ddl-rowsecurity.html", - "s8e65a2f3f2bb": "https://www.postgresql.org/docs/current/sql-createpolicy.html", - "sec39c317b06f": "https://www.postgresql.org/docs/current/sql-createfunction.html", - "s0fe66d71aa10": "https://www.postgresql.org/docs/current/sql-alterfunction.html", - "s04e81888353f": "https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html", - "sc39eceeac711": "https://www.postgresql.org/docs/current/functions-info.html" - } - }, - "postgres-view": { - "name": "PostgreSQL invoker-view minimum", - "basis": "15", - "sources": { - "s14443e063ef1": "https://www.postgresql.org/docs/release/15.0/" - } - }, - "curl": { - "name": "curl", - "basis": "curl-8_14_1", - "sources": { - "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", - "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md" - } - }, - "shellcheck": { - "name": "ShellCheck", - "basis": "v0.11.0", - "sources": { - "sf994764bf270": "https://github.com/koalaman/shellcheck/blob/v0.11.0/CHANGELOG.md#L1" - } - } - }, - "claims": { - "public-config": {"text": "Firebase API keys/config and Supabase publishable/anon keys are public identifiers, not authorization; hosted endpoints handle TLS.", "components": ["firebase", "supabase"], "sources": ["firebase:safeed4cd2b67", "firebase:sb0d26775f3b4", "supabase:s73d939b4644e"], "status": "REASONED"}, - "firebase-ownership": {"text": "Require authentication and ownership separately in Firestore, RTDB and Storage; authentication-only rules do not isolate users.", "components": ["firebase"], "sources": ["firebase:s100c2cdbdd59", "firebase:s53bfe8b21868"], "status": "REASONED"}, - "firebase-starting": {"text": "Firestore production and RTDB locked modes deny clients; test templates expose data for about a month. Inspect expiry and replace open rules.", "components": ["firebase"], "sources": ["firebase:s622d3bb0665b", "firebase:sb2c933c841ef", "firebase:s75f018e0fac7"], "status": "REASONED"}, - "firestore-rules": {"text": "Rules version 2 recursive wildcards include the user document and descendants; version 1 excludes the parent. Unmatched paths deny and overlapping grants can reopen access.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, - "rtdb-rules": {"text": "Owner rules under users.$uid need ancestor grants removed; child rules cannot revoke parent grants or filter parent reads.", "components": ["firebase"], "sources": ["firebase:sa6b1c4933e6a"], "status": "REASONED"}, - "firebase-storage": {"text": "Storage has separate rules; conflicting default prose/example warrants explicit deny-all followed by deliberate owner-path access.", "components": ["firebase"], "sources": ["firebase:s53bfe8b21868", "firebase:s0a7f54463abd"], "status": "REASONED"}, - "firebase-download": {"text": "Storage download URLs are non-expiring bearer credentials unaffected by tighter Rules; revoke tokens and use authorized getBlob/getBytes direct downloads.", "components": ["firebase"], "sources": ["firebase:s9b7e57b472c7", "firebase:sa0584b797732", "firebase:sfd1e7412abf6"], "status": "REASONED"}, - "firebase-rule-deploy": {"text": "Inspect and test deployed rules for every database/bucket/instance; keep local and deployed definitions synchronized.", "components": ["firebase"], "sources": ["firebase:sd6071548f6fe", "firebase:s14872143a9e8"], "status": "REASONED"}, - "firebase-server": {"text": "Admin SDK/server credentials bypass client Rules; keep credentials server-side and apply IAM and caller authorization.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, - "firebase-callable": {"text": "Callable ID-token validation is optional authentication; reject missing request.auth, use its UID and validate resource access. First-generation handlers use context.auth.", "components": ["firebase"], "sources": ["firebase:s79da8f65f90a", "firebase:sc8f214d059d2", "firebase:s9a86df9623a6"], "status": "REASONED"}, - "firebase-callable-appcheck": {"text": "onCall enforceAppCheck needs firebase-functions 4.0.0 or newer; App Check does not replace caller/resource authorization.", "components": ["firebase"], "sources": ["firebase:sb0ff8f1726f7"], "status": "REASONED"}, - "firebase-http": {"text": "onRequest does not inherit callable auth; verify end-user tokens with verifyIdToken or restrict IAM invokers, then authorize the intended principal.", "components": ["firebase"], "sources": ["firebase:s49f78d054708", "firebase:s1bb711c9afeb", "firebase:sc9f86ea271f6"], "status": "REASONED"}, - "firebase-appcheck": {"text": "Register compatible clients and inspect metrics, then enforce App Check per product and per callable; registration alone enforces nothing.", "components": ["firebase"], "sources": ["firebase:sa71e958c99df", "firebase:sba7129c83405", "firebase:sb0ff8f1726f7"], "status": "REASONED"}, - "firebase-key-restrictions": {"text": "Apply one app-restriction type per key plus API restrictions; separate platforms and unrelated billable APIs, using the full Firebase product table.", "components": ["firebase"], "sources": ["firebase:sc78d5eb59b65", "firebase:safeed4cd2b67"], "status": "REASONED"}, - "firebase-anonymous": {"text": "Anonymous sign-in is opt-in; disable unused providers because anonymous accounts still satisfy authentication-only Rules.", "components": ["firebase"], "sources": ["firebase:s77e2cbd8a29a"], "status": "REASONED"}, - "firebase-enumeration": {"text": "Email enumeration protection defaults on only for projects created from September 15, 2023; signup can still return EMAIL_EXISTS.", "components": ["firebase"], "sources": ["firebase:s06808e4b6965"], "status": "REASONED"}, - "firebase-emulators": {"text": "Emulators default to localhost but missing Firestore/RTDB/Storage Rules configuration means open data; keep listeners, UI, hub, proxies and tunnels private.", "components": ["firebase"], "sources": ["firebase:sd8a1bda29f5c", "firebase:s69d9721c7f29"], "status": "REASONED"}, - "supabase-network": {"text": "Restrict hosted database/pooler client IPs with Network Restrictions, enforce SSL and protect database-role passwords.", "components": ["supabase"], "sources": ["supabase:sb787b5d63633", "supabase:s83e15676e1ff", "supabase:sf69cafb4bc6c"], "status": "REASONED"}, - "supabase-grants": {"text": "Data API access needs grants plus RLS; automatic-grant defaults are transitioning. Table Editor enables RLS, while SQL creation needs explicit enablement.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3"], "status": "REASONED"}, - "supabase-select": {"text": "The profiles baseline enables RLS, revokes broad grants and permits authenticated owners SELECT; audit inherited/column grants and existing policies.", "components": ["supabase", "postgres"], "sources": ["supabase:s46cff2590323", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, - "supabase-bypass": {"text": "With RLS enabled and no policy, ordinary subject roles get no rows; owners normally bypass, as do superusers/BYPASSRLS roles. TRUNCATE is outside RLS.", "components": ["postgres"], "sources": ["postgres:se9846041ce1f"], "status": "REASONED"}, - "supabase-writes": {"text": "Grant and policy INSERT/UPDATE/DELETE only when intended; UPDATE checks old/new ownership and needs applicable SELECT when reading columns.", "components": ["supabase", "postgres"], "sources": ["supabase:s46cff2590323", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, - "supabase-schemas": {"text": "public is exposed by default; restrict exposed schemas and deliberately grant schema/object access. Schema selection does not replace RLS.", "components": ["supabase"], "sources": ["supabase:s20ef046ac646"], "status": "REASONED"}, - "supabase-api-disable": {"text": "Disable an unused Data API to close generated REST endpoints; this does not disable all other services.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3"], "status": "REASONED"}, - "supabase-keys": {"text": "Publishable keys select anon or authenticated by session; secret and legacy service_role keys bypass RLS and belong only on trusted servers.", "components": ["supabase"], "sources": ["supabase:s73d939b4644e"], "status": "REASONED"}, - "supabase-key-retire": {"text": "Replacement keys do not disable legacy keys; migrate consumers then explicitly deactivate legacy keys, or delete compromised secret keys.", "components": ["supabase"], "sources": ["supabase:sa946354db383"], "status": "REASONED"}, - "supabase-jwt-retire": {"text": "Rotation changes the signing key but retains previous trust until revocation; account for verifier caches and propagation.", "components": ["supabase"], "sources": ["supabase:sfc2f9ae0232b"], "status": "REASONED"}, - "supabase-view-owner": {"text": "Views use owner privileges/policies by default; disclosure depends on reachable schemas, grants, query and owner privileges, not anon-key possession alone.", "components": ["postgres", "supabase"], "sources": ["postgres:s13c54ab9cc6b", "supabase:s46cff2590323"], "status": "REASONED"}, - "supabase-view-invoker": {"text": "PostgreSQL 15+ security_invoker views apply caller policies and require underlying privileges.", "components": ["postgres-view", "postgres"], "sources": ["postgres-view:s14443e063ef1", "postgres:saa99ef28f60f", "postgres:s13c54ab9cc6b"], "status": "REASONED"}, - "supabase-view-legacy": {"text": "PostgreSQL 14 and earlier require revoking all view privileges from PUBLIC/anon/authenticated or unexposing the schema; updatable views can bypass RLS for writes.", "components": ["postgres"], "sources": ["postgres:s13c54ab9cc6b", "postgres:sb6224f4ac866", "postgres:sc39eceeac711"], "status": "REASONED"}, - "supabase-functions": {"text": "Prefer INVOKER; restrict DEFINER execution, use empty search_path with qualified references and enforce caller authorization including required MFA.", "components": ["supabase", "postgres"], "sources": ["supabase:s8405a9fb4b74", "postgres:sec39c317b06f", "postgres:s0fe66d71aa10"], "status": "REASONED"}, - "supabase-function-defaults": {"text": "New functions grant PUBLIC execution; create privileged functions and restrict grants transactionally. Global/default revocations are creator-specific and do not fix existing functions.", "components": ["postgres"], "sources": ["postgres:sec39c317b06f", "postgres:s04e81888353f"], "status": "REASONED"}, - "supabase-http": {"text": "Audit http/pg_net wrappers and revoke unneeded execute grants; constrain callers/destinations/data. Extension enablement alone does not prove exploitable SSRF.", "components": ["supabase"], "sources": ["supabase:s13da01bbbba5", "supabase:s76740bffcb32"], "status": "REASONED"}, - "supabase-vault": {"text": "Restrict vault.decrypted_secrets and privileged wrappers and keep Vault unexposed; at-rest encryption cannot prevent authorized plaintext queries.", "components": ["supabase"], "sources": ["supabase:s47bb8fa3e14e"], "status": "REASONED"}, - "supabase-buckets": {"text": "Private downloads need authorized access or time-limited signed URLs; public buckets bypass retrieval checks but not upload/delete/move/copy controls.", "components": ["supabase"], "sources": ["supabase:s1567fb838f20"], "status": "REASONED"}, - "supabase-storage-policies": {"text": "Authorize storage.objects separately by bucket and owner path; upload needs INSERT, upsert also SELECT/UPDATE, and deletion needs deliberate permission.", "components": ["supabase"], "sources": ["supabase:sdba7d7b6dcf8", "supabase:s9e1484f46eff", "supabase:see2d3be99ebd"], "status": "REASONED"}, - "supabase-realtime-default": {"text": "Public Realtime channels are allowed by default; disable them project-wide and also set client config.private.", "components": ["supabase"], "sources": ["supabase:s5674b1057e14"], "status": "REASONED"}, - "supabase-realtime-policy": {"text": "Broadcast/Presence receive/send need separate SELECT/INSERT policies on realtime.messages, constrained by topic ownership and extension.", "components": ["supabase"], "sources": ["supabase:s84efad0ba39d"], "status": "REASONED"}, - "supabase-changes": {"text": "Postgres Changes uses source-table authorization; private-channel policies are not universal and DELETE has separate limitations.", "components": ["supabase"], "sources": ["supabase:s694eab64f2ec"], "status": "REASONED"}, - "supabase-signup": {"text": "Disable signup, anonymous sign-ins and unused providers when unwanted; inspect hosted settings instead of assuming universal defaults.", "components": ["supabase"], "sources": ["supabase:sc797f0b63472"], "status": "REASONED"}, - "supabase-cli-auth": {"text": "CLI enable_signup defaults true and enable_anonymous_sign_ins false; local config edits do not prove hosted settings changed.", "components": ["supabase"], "sources": ["supabase:s3a5e39fe7b4d"], "status": "REASONED"}, - "supabase-mfa": {"text": "TOTP is available on every plan at writing; enrollment alone does not constrain data. Require aal2 using a restrictive policy alongside permissive ownership policies.", "components": ["supabase", "postgres"], "sources": ["supabase:s182755725d74", "supabase:s741468ff35a9", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, - "supabase-edge-jwt": {"text": "Per-function verify_jwt defaults true; --no-verify-jwt disables platform checking and requires handler authentication before protected work.", "components": ["supabase"], "sources": ["supabase:s7dfbb47b5d20", "supabase:sf7d438b8c080", "supabase:se52bbd097033"], "status": "REASONED"}, - "supabase-edge-auth": {"text": "Platform JWT success is not resource authorization and API-key compatibility can pass it; separately authorize users or verify webhook signatures.", "components": ["supabase"], "sources": ["supabase:sf7d438b8c080", "supabase:se52bbd097033"], "status": "REASONED"}, - "supabase-edge-secrets": {"text": "Edge environments contain privileged credentials including SUPABASE_SERVICE_ROLE_KEY; protect managed/local secrets, responses, logs and client bundles.", "components": ["supabase"], "sources": ["supabase:sb3c4c0993e14"], "status": "REASONED"}, - "verify-firestore": {"text": "Compare unsigned/A/B reads and writes to user documents and descendants, with a successful owner control; test parent documents to distinguish recursive-rule versions.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, - "verify-rtdb": {"text": "Compare unsigned/A/B reads/writes to seeded RTDB owner paths; ancestor grants expose them while ownership rules deny unauthorized callers.", "components": ["firebase"], "sources": ["firebase:sa6b1c4933e6a"], "status": "REASONED"}, - "verify-firebase-storage": {"text": "Compare unsigned/A/B object reads and uploads; ownership rules must deny unrelated callers while owner operations succeed.", "components": ["firebase"], "sources": ["firebase:s0a7f54463abd"], "status": "REASONED"}, - "verify-download-token": {"text": "Same sessionless download URL works after Rules tightening but must lose bytes after token revocation; owner getBytes remains successful and network/CORS failures are inconclusive.", "components": ["firebase"], "sources": ["firebase:s9b7e57b472c7", "firebase:sa0584b797732", "firebase:sfd1e7412abf6"], "status": "REASONED"}, - "verify-tables": {"text": "Pair unsigned/A/B profiles GETs against seeded rows, using ordinary credentials and required MFA; test intended writes and persisted effects separately.", "components": ["supabase", "postgres", "curl"], "sources": ["supabase:s92b405dc3c7e", "postgres:se9846041ce1f", "supabase:s741468ff35a9", "curl:s5e1383feef7d", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, - "verify-views": {"text": "Compare view reads with table boundaries; owner-privileged exposure must close under invoker/revoked access while intended reads or deliberate removal are confirmed.", "components": ["postgres"], "sources": ["postgres:s13c54ab9cc6b"], "status": "REASONED"}, - "verify-rpc": {"text": "Test every API function overload with valid harmless arguments as unsigned/A/B; unauthorized data or operations must be denied while the intended caller succeeds.", "components": ["supabase"], "sources": ["supabase:s087c8cf42ab0", "supabase:s8405a9fb4b74"], "status": "REASONED"}, - "verify-http": {"text": "Probe HTTP wrappers with a controlled collector and marker; unauthorized fixed calls must emit no request. Commit pg_net fixture transactions before expecting delivery.", "components": ["supabase"], "sources": ["supabase:s76740bffcb32"], "status": "REASONED"}, - "verify-vault": {"text": "Use a disposable Vault canary to compare exposed/fixed wrappers; inspect function and table privileges as supporting evidence.", "components": ["supabase", "postgres"], "sources": ["supabase:s47bb8fa3e14e", "postgres:sc39eceeac711"], "status": "REASONED"}, - "verify-storage": {"text": "Compare public URL and A/B private downloads plus intended uploads/upserts; unauthorized callers must receive no bytes or cross-owner writes.", "components": ["supabase"], "sources": ["supabase:sc29febbb230e", "supabase:see2d3be99ebd", "supabase:s1567fb838f20"], "status": "REASONED"}, - "verify-realtime": {"text": "Compare A/B join, Broadcast and Presence delivery; public-disabled clients lacking private=true must fail, and connection alone is insufficient.", "components": ["supabase"], "sources": ["supabase:s5674b1057e14", "supabase:s84efad0ba39d"], "status": "REASONED"}, - "verify-changes": {"text": "Observe A/B INSERT/UPDATE subscription events against seeded rows; fixed table authorization withholds unrelated events. Evaluate DELETE separately.", "components": ["supabase"], "sources": ["supabase:s694eab64f2ec"], "status": "REASONED"}, - "verify-edge": {"text": "Compare no/invalid Bearer without apikey, authorized owners and valid webhook signatures; distinguish platform 401 from handler authorization and inspect effects/secrets.", "components": ["supabase"], "sources": ["supabase:se52bbd097033", "supabase:sf7d438b8c080", "supabase:sb3c4c0993e14"], "status": "REASONED"}, - "verify-firebase-functions": {"text": "Compare callable requests with valid App Check but missing user auth and A/B operations; HTTP endpoints need separate invalid/valid user or IAM comparisons.", "components": ["firebase"], "sources": ["firebase:s79da8f65f90a", "firebase:s49f78d054708"], "status": "REASONED"}, - "verify-appcheck": {"text": "Repeat owner requests with/without App Check per product/callable; enforcement must deny missing attestation while compatible clients work.", "components": ["firebase"], "sources": ["firebase:sba7129c83405", "firebase:sb0ff8f1726f7"], "status": "REASONED"}, - "verify-api-restrictions": {"text": "Test intended versus excluded apps/referrers after key restrictions, retaining successful intended product traffic.", "components": ["firebase"], "sources": ["firebase:safeed4cd2b67", "firebase:sc78d5eb59b65"], "status": "REASONED"}, - "verify-emulators": {"text": "Enumerate hub /emulators and sockets, compare missing versus loaded Rules and second-host connectivity, and retain local owner controls.", "components": ["firebase"], "sources": ["firebase:sd8a1bda29f5c", "firebase:s69d9721c7f29"], "status": "REASONED"}, - "verify-firebase-accounts": {"text": "Sign out before each Anonymous signup test and check isNewUser; disabled providers must reject creation. Enumeration tests compare sign-in errors, not EMAIL_EXISTS signup behavior.", "components": ["firebase"], "sources": ["firebase:s8c90f9f6c7fa", "firebase:s8c8336ec252e", "firebase:s142e56472093", "firebase:s77e2cbd8a29a", "firebase:s06808e4b6965"], "status": "REASONED"}, - "verify-supabase-accounts": {"text": "Fresh signup and anonymous creation must fail under invite-only/anonymous-disabled settings while an invited account still signs in.", "components": ["supabase"], "sources": ["supabase:sc797f0b63472", "supabase:sa2dd4637969e", "supabase:sad79d4d1b6ee"], "status": "REASONED"}, - "verify-mfa": {"text": "Owner profiles GET with aal1 must yield no protected rows under the restrictive policy, while fresh aal2 returns the seeded owner row.", "components": ["supabase"], "sources": ["supabase:s741468ff35a9"], "status": "REASONED", "verify": [1]}, - "verify-key-retirement": {"text": "Compare old/new API keys after explicit deactivation and old/new JWTs after revocation, accounting for propagation and caches.", "components": ["supabase"], "sources": ["supabase:sa946354db383", "supabase:sfc2f9ae0232b"], "status": "REASONED"}, - "verify-api-removal": {"text": "Previously successful profiles requests must stop returning fixtures after schema removal or Data API disabling; retained schemas need authorized controls.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3", "supabase:s20ef046ac646"], "status": "REASONED"}, - "verify-bundle": {"text": "Filename-only service_role/eyJ/sb_secret_ scanning yields review candidates, not proof of leaks; public anon JWTs are legitimate. Use a marker control and inspect encoded/split secrets separately.", "components": ["supabase"], "sources": ["supabase:s73d939b4644e"], "status": "REASONED", "verify": [2]}, - "local-guards": {"text": "Historical local guard tests rejected placeholders and malformed set lines; this guide's assessment is shell behavior only, not live authorization. The recorded ShellCheck 0.11.0 has a release citation, not host-binary evidence; bash and other tool/parser versions remain unrecorded.", "components": ["supabase", "shellcheck"], "sources": ["supabase:s92b405dc3c7e", "supabase:s73d939b4644e", "shellcheck:sf994764bf270"], "status": "DEMONSTRATED", "evidence": "Local guard tests refused embedded `REPLACE_WITH_` placeholders, `example.com`, angle brackets, empty values, and omitted or shortened `set --` lines."}, - "local-syntax": {"text": "Recorded ShellCheck 0.11.0/bash and JSON/TOML/JavaScript parsing passed; this guide's assessment is that these checks do not compile Firebase Rules, execute SQL or demonstrate services. Only the ShellCheck release is cited; bash and other tool/parser versions remain unrecorded.", "components": ["firebase", "supabase", "shellcheck"], "sources": ["firebase:sa6b1c4933e6a", "firebase:sd8a1bda29f5c", "supabase:s3a5e39fe7b4d", "firebase:sc8f214d059d2", "shellcheck:sf994764bf270"], "status": "DEMONSTRATED", "evidence": "The RTDB JSON, `firebase.json` fragment, Auth TOML, and callable JavaScript passed local parsing checks."} - } -} ---- # Firebase and Supabase: the rules are the security - -**Version basis** +Written against Firebase documentation (no recorded version), PostgreSQL invoker-view minimum 15, curl curl-8_14_1, and ShellCheck v0.11.0; 2 further sources carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 70 claims: 2 demonstrated by a check that ran and 68 reasoned from cited sources. -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +These platforms handle TLS for you on their hosted endpoints; the exposure works differently. +Client SDKs talk to the backend using keys that ship in your frontend code and are **public by +design**: the Firebase API key and Supabase publishable or legacy `anon` key. Client data access +needs Firebase Security Rules or Postgres row-level security (RLS) alongside grants, IAM, +endpoint authorization, and App Check where applicable. AI-generated apps repeatedly ship with +that layer open because "it worked in testing". On Supabase the gate also depends on how exposed +views and functions execute, not only on the table policies. -Components (recorded basis): +Documentation was checked in September 2026. Defaults below describe the documentation at the +time of writing; inspect the effective settings of each project. Live behavior has not been +demonstrated in this authoring environment. The Verify section records the missing prerequisites +and the vendor documentation supporting the REASONED checks. -- firebase: Firebase documentation (unknown) -- supabase: Supabase documentation (unknown) -- postgres: PostgreSQL documentation (unknown) -- postgres-view: PostgreSQL invoker-view minimum (15) -- curl: curl (curl-8_14_1) -- shellcheck: ShellCheck (v0.11.0) +## Firebase -Claims (full source mapping in the front matter): - -- public-config (REASONED; firebase, supabase): Firebase API keys/config and Supabase publishable/anon keys are public identifiers, not authorization; hosted endpoints handle TLS. -- firebase-ownership (REASONED; firebase): Require authentication and ownership separately in Firestore, RTDB and Storage; authentication-only rules do not isolate users. -- firebase-starting (REASONED; firebase): Firestore production and RTDB locked modes deny clients; test templates expose data for about a month. Inspect expiry and replace open rules. -- firestore-rules (REASONED; firebase): Rules version 2 recursive wildcards include the user document and descendants; version 1 excludes the parent. Unmatched paths deny and overlapping grants can reopen access. -- rtdb-rules (REASONED; firebase): Owner rules under users.$uid need ancestor grants removed; child rules cannot revoke parent grants or filter parent reads. -- firebase-storage (REASONED; firebase): Storage has separate rules; conflicting default prose/example warrants explicit deny-all followed by deliberate owner-path access. -- firebase-download (REASONED; firebase): Storage download URLs are non-expiring bearer credentials unaffected by tighter Rules; revoke tokens and use authorized getBlob/getBytes direct downloads. -- firebase-rule-deploy (REASONED; firebase): Inspect and test deployed rules for every database/bucket/instance; keep local and deployed definitions synchronized. -- firebase-server (REASONED; firebase): Admin SDK/server credentials bypass client Rules; keep credentials server-side and apply IAM and caller authorization. -- firebase-callable (REASONED; firebase): Callable ID-token validation is optional authentication; reject missing request.auth, use its UID and validate resource access. First-generation handlers use context.auth. -- firebase-callable-appcheck (REASONED; firebase): onCall enforceAppCheck needs firebase-functions 4.0.0 or newer; App Check does not replace caller/resource authorization. -- firebase-http (REASONED; firebase): onRequest does not inherit callable auth; verify end-user tokens with verifyIdToken or restrict IAM invokers, then authorize the intended principal. -- firebase-appcheck (REASONED; firebase): Register compatible clients and inspect metrics, then enforce App Check per product and per callable; registration alone enforces nothing. -- firebase-key-restrictions (REASONED; firebase): Apply one app-restriction type per key plus API restrictions; separate platforms and unrelated billable APIs, using the full Firebase product table. -- firebase-anonymous (REASONED; firebase): Anonymous sign-in is opt-in; disable unused providers because anonymous accounts still satisfy authentication-only Rules. -- firebase-enumeration (REASONED; firebase): Email enumeration protection defaults on only for projects created from September 15, 2023; signup can still return EMAIL_EXISTS. -- firebase-emulators (REASONED; firebase): Emulators default to localhost but missing Firestore/RTDB/Storage Rules configuration means open data; keep listeners, UI, hub, proxies and tunnels private. -- supabase-network (REASONED; supabase): Restrict hosted database/pooler client IPs with Network Restrictions, enforce SSL and protect database-role passwords. -- supabase-grants (REASONED; supabase): Data API access needs grants plus RLS; automatic-grant defaults are transitioning. Table Editor enables RLS, while SQL creation needs explicit enablement. -- supabase-select (REASONED; supabase, postgres): The profiles baseline enables RLS, revokes broad grants and permits authenticated owners SELECT; audit inherited/column grants and existing policies. -- supabase-bypass (REASONED; postgres): With RLS enabled and no policy, ordinary subject roles get no rows; owners normally bypass, as do superusers/BYPASSRLS roles. TRUNCATE is outside RLS. -- supabase-writes (REASONED; supabase, postgres): Grant and policy INSERT/UPDATE/DELETE only when intended; UPDATE checks old/new ownership and needs applicable SELECT when reading columns. -- supabase-schemas (REASONED; supabase): public is exposed by default; restrict exposed schemas and deliberately grant schema/object access. Schema selection does not replace RLS. -- supabase-api-disable (REASONED; supabase): Disable an unused Data API to close generated REST endpoints; this does not disable all other services. -- supabase-keys (REASONED; supabase): Publishable keys select anon or authenticated by session; secret and legacy service_role keys bypass RLS and belong only on trusted servers. -- supabase-key-retire (REASONED; supabase): Replacement keys do not disable legacy keys; migrate consumers then explicitly deactivate legacy keys, or delete compromised secret keys. -- supabase-jwt-retire (REASONED; supabase): Rotation changes the signing key but retains previous trust until revocation; account for verifier caches and propagation. -- supabase-view-owner (REASONED; postgres, supabase): Views use owner privileges/policies by default; disclosure depends on reachable schemas, grants, query and owner privileges, not anon-key possession alone. -- supabase-view-invoker (REASONED; postgres-view, postgres): PostgreSQL 15+ security_invoker views apply caller policies and require underlying privileges. -- supabase-view-legacy (REASONED; postgres): PostgreSQL 14 and earlier require revoking all view privileges from PUBLIC/anon/authenticated or unexposing the schema; updatable views can bypass RLS for writes. -- supabase-functions (REASONED; supabase, postgres): Prefer INVOKER; restrict DEFINER execution, use empty search_path with qualified references and enforce caller authorization including required MFA. -- supabase-function-defaults (REASONED; postgres): New functions grant PUBLIC execution; create privileged functions and restrict grants transactionally. Global/default revocations are creator-specific and do not fix existing functions. -- supabase-http (REASONED; supabase): Audit http/pg_net wrappers and revoke unneeded execute grants; constrain callers/destinations/data. Extension enablement alone does not prove exploitable SSRF. -- supabase-vault (REASONED; supabase): Restrict vault.decrypted_secrets and privileged wrappers and keep Vault unexposed; at-rest encryption cannot prevent authorized plaintext queries. -- supabase-buckets (REASONED; supabase): Private downloads need authorized access or time-limited signed URLs; public buckets bypass retrieval checks but not upload/delete/move/copy controls. -- supabase-storage-policies (REASONED; supabase): Authorize storage.objects separately by bucket and owner path; upload needs INSERT, upsert also SELECT/UPDATE, and deletion needs deliberate permission. -- supabase-realtime-default (REASONED; supabase): Public Realtime channels are allowed by default; disable them project-wide and also set client config.private. -- supabase-realtime-policy (REASONED; supabase): Broadcast/Presence receive/send need separate SELECT/INSERT policies on realtime.messages, constrained by topic ownership and extension. -- supabase-changes (REASONED; supabase): Postgres Changes uses source-table authorization; private-channel policies are not universal and DELETE has separate limitations. -- supabase-signup (REASONED; supabase): Disable signup, anonymous sign-ins and unused providers when unwanted; inspect hosted settings instead of assuming universal defaults. -- supabase-cli-auth (REASONED; supabase): CLI enable_signup defaults true and enable_anonymous_sign_ins false; local config edits do not prove hosted settings changed. -- supabase-mfa (REASONED; supabase, postgres): TOTP is available on every plan at writing; enrollment alone does not constrain data. Require aal2 using a restrictive policy alongside permissive ownership policies. -- supabase-edge-jwt (REASONED; supabase): Per-function verify_jwt defaults true; --no-verify-jwt disables platform checking and requires handler authentication before protected work. -- supabase-edge-auth (REASONED; supabase): Platform JWT success is not resource authorization and API-key compatibility can pass it; separately authorize users or verify webhook signatures. -- supabase-edge-secrets (REASONED; supabase): Edge environments contain privileged credentials including SUPABASE_SERVICE_ROLE_KEY; protect managed/local secrets, responses, logs and client bundles. -- verify-firestore (REASONED; firebase): Compare unsigned/A/B reads and writes to user documents and descendants, with a successful owner control; test parent documents to distinguish recursive-rule versions. -- verify-rtdb (REASONED; firebase): Compare unsigned/A/B reads/writes to seeded RTDB owner paths; ancestor grants expose them while ownership rules deny unauthorized callers. -- verify-firebase-storage (REASONED; firebase): Compare unsigned/A/B object reads and uploads; ownership rules must deny unrelated callers while owner operations succeed. -- verify-download-token (REASONED; firebase): Same sessionless download URL works after Rules tightening but must lose bytes after token revocation; owner getBytes remains successful and network/CORS failures are inconclusive. -- verify-tables (REASONED; supabase, postgres, curl): Pair unsigned/A/B profiles GETs against seeded rows, using ordinary credentials and required MFA; test intended writes and persisted effects separately. -- verify-views (REASONED; postgres): Compare view reads with table boundaries; owner-privileged exposure must close under invoker/revoked access while intended reads or deliberate removal are confirmed. -- verify-rpc (REASONED; supabase): Test every API function overload with valid harmless arguments as unsigned/A/B; unauthorized data or operations must be denied while the intended caller succeeds. -- verify-http (REASONED; supabase): Probe HTTP wrappers with a controlled collector and marker; unauthorized fixed calls must emit no request. Commit pg_net fixture transactions before expecting delivery. -- verify-vault (REASONED; supabase, postgres): Use a disposable Vault canary to compare exposed/fixed wrappers; inspect function and table privileges as supporting evidence. -- verify-storage (REASONED; supabase): Compare public URL and A/B private downloads plus intended uploads/upserts; unauthorized callers must receive no bytes or cross-owner writes. -- verify-realtime (REASONED; supabase): Compare A/B join, Broadcast and Presence delivery; public-disabled clients lacking private=true must fail, and connection alone is insufficient. -- verify-changes (REASONED; supabase): Observe A/B INSERT/UPDATE subscription events against seeded rows; fixed table authorization withholds unrelated events. Evaluate DELETE separately. -- verify-edge (REASONED; supabase): Compare no/invalid Bearer without apikey, authorized owners and valid webhook signatures; distinguish platform 401 from handler authorization and inspect effects/secrets. -- verify-firebase-functions (REASONED; firebase): Compare callable requests with valid App Check but missing user auth and A/B operations; HTTP endpoints need separate invalid/valid user or IAM comparisons. -- verify-appcheck (REASONED; firebase): Repeat owner requests with/without App Check per product/callable; enforcement must deny missing attestation while compatible clients work. -- verify-api-restrictions (REASONED; firebase): Test intended versus excluded apps/referrers after key restrictions, retaining successful intended product traffic. -- verify-emulators (REASONED; firebase): Enumerate hub /emulators and sockets, compare missing versus loaded Rules and second-host connectivity, and retain local owner controls. -- verify-firebase-accounts (REASONED; firebase): Sign out before each Anonymous signup test and check isNewUser; disabled providers must reject creation. Enumeration tests compare sign-in errors, not EMAIL_EXISTS signup behavior. -- verify-supabase-accounts (REASONED; supabase): Fresh signup and anonymous creation must fail under invite-only/anonymous-disabled settings while an invited account still signs in. -- verify-mfa (REASONED; supabase): Owner profiles GET with aal1 must yield no protected rows under the restrictive policy, while fresh aal2 returns the seeded owner row. -- verify-key-retirement (REASONED; supabase): Compare old/new API keys after explicit deactivation and old/new JWTs after revocation, accounting for propagation and caches. -- verify-api-removal (REASONED; supabase): Previously successful profiles requests must stop returning fixtures after schema removal or Data API disabling; retained schemas need authorized controls. -- verify-bundle (REASONED; supabase): Filename-only service_role/eyJ/sb_secret_ scanning yields review candidates, not proof of leaks; public anon JWTs are legitimate. Use a marker control and inspect encoded/split secrets separately. -- local-guards (DEMONSTRATED; supabase, shellcheck): Historical local guard tests rejected placeholders and malformed set lines; this guide's assessment is shell behavior only, not live authorization. The recorded ShellCheck 0.11.0 has a release citation, not host-binary evidence; bash and other tool/parser versions remain unrecorded. -- local-syntax (DEMONSTRATED; firebase, supabase, shellcheck): Recorded ShellCheck 0.11.0/bash and JSON/TOML/JavaScript parsing passed; this guide's assessment is that these checks do not compile Firebase Rules, execute SQL or demonstrate services. Only the ShellCheck release is cited; bash and other tool/parser versions remain unrecorded. - - -These platforms handle TLS for you on their hosted endpoints; the exposure works differently. -Client SDKs talk to the backend using keys that ship in your frontend code and are **public by -design**: the Firebase API key and Supabase publishable or legacy `anon` key. Client data access -needs Firebase Security Rules or Postgres row-level security (RLS) alongside grants, IAM, -endpoint authorization, and App Check where applicable. AI-generated apps repeatedly ship with -that layer open because "it worked in testing". On Supabase the gate also depends on how exposed -views and functions execute, not only on the table policies. - -Documentation was checked in September 2026. Defaults below describe the documentation at the -time of writing; inspect the effective settings of each project. Live behavior has not been -demonstrated in this authoring environment. The Verify section records the missing prerequisites -and the vendor documentation supporting the REASONED checks. - -## Firebase - -### 1. Set ownership rules in all three products +### 1. Set ownership rules in all three products Every Firestore, Realtime Database, and Storage instance needs explicit security rules. Never deploy the all-open rule (`allow read, write: if true;` or `".read": true, ".write": true`); @@ -1235,3 +952,312 @@ REASONED: tool-version provenance for the historical local runs above; the [Shel - [curl curl-8_14_1 write-out fields and version requirements (checked October 2026)](https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79). - [curl curl-8_14_1 headers from stdin (checked October 2026)](https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md). - [ShellCheck v0.11.0 changelog release entry (checked October 2026)](https://github.com/koalaman/shellcheck/blob/v0.11.0/CHANGELOG.md#L1). + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/firebase-supabase.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- firebase: Firebase documentation (unknown) +- supabase: Supabase documentation (unknown) +- postgres: PostgreSQL documentation (unknown) +- postgres-view: PostgreSQL invoker-view minimum (15) +- curl: curl (curl-8_14_1) +- shellcheck: ShellCheck (v0.11.0) + +Claims (full source mapping in the JSON block below): + +- public-config (REASONED; firebase, supabase): Firebase API keys/config and Supabase publishable/anon keys are public identifiers, not authorization; hosted endpoints handle TLS. +- firebase-ownership (REASONED; firebase): Require authentication and ownership separately in Firestore, RTDB and Storage; authentication-only rules do not isolate users. +- firebase-starting (REASONED; firebase): Firestore production and RTDB locked modes deny clients; test templates expose data for about a month. Inspect expiry and replace open rules. +- firestore-rules (REASONED; firebase): Rules version 2 recursive wildcards include the user document and descendants; version 1 excludes the parent. Unmatched paths deny and overlapping grants can reopen access. +- rtdb-rules (REASONED; firebase): Owner rules under users.$uid need ancestor grants removed; child rules cannot revoke parent grants or filter parent reads. +- firebase-storage (REASONED; firebase): Storage has separate rules; conflicting default prose/example warrants explicit deny-all followed by deliberate owner-path access. +- firebase-download (REASONED; firebase): Storage download URLs are non-expiring bearer credentials unaffected by tighter Rules; revoke tokens and use authorized getBlob/getBytes direct downloads. +- firebase-rule-deploy (REASONED; firebase): Inspect and test deployed rules for every database/bucket/instance; keep local and deployed definitions synchronized. +- firebase-server (REASONED; firebase): Admin SDK/server credentials bypass client Rules; keep credentials server-side and apply IAM and caller authorization. +- firebase-callable (REASONED; firebase): Callable ID-token validation is optional authentication; reject missing request.auth, use its UID and validate resource access. First-generation handlers use context.auth. +- firebase-callable-appcheck (REASONED; firebase): onCall enforceAppCheck needs firebase-functions 4.0.0 or newer; App Check does not replace caller/resource authorization. +- firebase-http (REASONED; firebase): onRequest does not inherit callable auth; verify end-user tokens with verifyIdToken or restrict IAM invokers, then authorize the intended principal. +- firebase-appcheck (REASONED; firebase): Register compatible clients and inspect metrics, then enforce App Check per product and per callable; registration alone enforces nothing. +- firebase-key-restrictions (REASONED; firebase): Apply one app-restriction type per key plus API restrictions; separate platforms and unrelated billable APIs, using the full Firebase product table. +- firebase-anonymous (REASONED; firebase): Anonymous sign-in is opt-in; disable unused providers because anonymous accounts still satisfy authentication-only Rules. +- firebase-enumeration (REASONED; firebase): Email enumeration protection defaults on only for projects created from September 15, 2023; signup can still return EMAIL_EXISTS. +- firebase-emulators (REASONED; firebase): Emulators default to localhost but missing Firestore/RTDB/Storage Rules configuration means open data; keep listeners, UI, hub, proxies and tunnels private. +- supabase-network (REASONED; supabase): Restrict hosted database/pooler client IPs with Network Restrictions, enforce SSL and protect database-role passwords. +- supabase-grants (REASONED; supabase): Data API access needs grants plus RLS; automatic-grant defaults are transitioning. Table Editor enables RLS, while SQL creation needs explicit enablement. +- supabase-select (REASONED; supabase, postgres): The profiles baseline enables RLS, revokes broad grants and permits authenticated owners SELECT; audit inherited/column grants and existing policies. +- supabase-bypass (REASONED; postgres): With RLS enabled and no policy, ordinary subject roles get no rows; owners normally bypass, as do superusers/BYPASSRLS roles. TRUNCATE is outside RLS. +- supabase-writes (REASONED; supabase, postgres): Grant and policy INSERT/UPDATE/DELETE only when intended; UPDATE checks old/new ownership and needs applicable SELECT when reading columns. +- supabase-schemas (REASONED; supabase): public is exposed by default; restrict exposed schemas and deliberately grant schema/object access. Schema selection does not replace RLS. +- supabase-api-disable (REASONED; supabase): Disable an unused Data API to close generated REST endpoints; this does not disable all other services. +- supabase-keys (REASONED; supabase): Publishable keys select anon or authenticated by session; secret and legacy service_role keys bypass RLS and belong only on trusted servers. +- supabase-key-retire (REASONED; supabase): Replacement keys do not disable legacy keys; migrate consumers then explicitly deactivate legacy keys, or delete compromised secret keys. +- supabase-jwt-retire (REASONED; supabase): Rotation changes the signing key but retains previous trust until revocation; account for verifier caches and propagation. +- supabase-view-owner (REASONED; postgres, supabase): Views use owner privileges/policies by default; disclosure depends on reachable schemas, grants, query and owner privileges, not anon-key possession alone. +- supabase-view-invoker (REASONED; postgres-view, postgres): PostgreSQL 15+ security_invoker views apply caller policies and require underlying privileges. +- supabase-view-legacy (REASONED; postgres): PostgreSQL 14 and earlier require revoking all view privileges from PUBLIC/anon/authenticated or unexposing the schema; updatable views can bypass RLS for writes. +- supabase-functions (REASONED; supabase, postgres): Prefer INVOKER; restrict DEFINER execution, use empty search_path with qualified references and enforce caller authorization including required MFA. +- supabase-function-defaults (REASONED; postgres): New functions grant PUBLIC execution; create privileged functions and restrict grants transactionally. Global/default revocations are creator-specific and do not fix existing functions. +- supabase-http (REASONED; supabase): Audit http/pg_net wrappers and revoke unneeded execute grants; constrain callers/destinations/data. Extension enablement alone does not prove exploitable SSRF. +- supabase-vault (REASONED; supabase): Restrict vault.decrypted_secrets and privileged wrappers and keep Vault unexposed; at-rest encryption cannot prevent authorized plaintext queries. +- supabase-buckets (REASONED; supabase): Private downloads need authorized access or time-limited signed URLs; public buckets bypass retrieval checks but not upload/delete/move/copy controls. +- supabase-storage-policies (REASONED; supabase): Authorize storage.objects separately by bucket and owner path; upload needs INSERT, upsert also SELECT/UPDATE, and deletion needs deliberate permission. +- supabase-realtime-default (REASONED; supabase): Public Realtime channels are allowed by default; disable them project-wide and also set client config.private. +- supabase-realtime-policy (REASONED; supabase): Broadcast/Presence receive/send need separate SELECT/INSERT policies on realtime.messages, constrained by topic ownership and extension. +- supabase-changes (REASONED; supabase): Postgres Changes uses source-table authorization; private-channel policies are not universal and DELETE has separate limitations. +- supabase-signup (REASONED; supabase): Disable signup, anonymous sign-ins and unused providers when unwanted; inspect hosted settings instead of assuming universal defaults. +- supabase-cli-auth (REASONED; supabase): CLI enable_signup defaults true and enable_anonymous_sign_ins false; local config edits do not prove hosted settings changed. +- supabase-mfa (REASONED; supabase, postgres): TOTP is available on every plan at writing; enrollment alone does not constrain data. Require aal2 using a restrictive policy alongside permissive ownership policies. +- supabase-edge-jwt (REASONED; supabase): Per-function verify_jwt defaults true; --no-verify-jwt disables platform checking and requires handler authentication before protected work. +- supabase-edge-auth (REASONED; supabase): Platform JWT success is not resource authorization and API-key compatibility can pass it; separately authorize users or verify webhook signatures. +- supabase-edge-secrets (REASONED; supabase): Edge environments contain privileged credentials including SUPABASE_SERVICE_ROLE_KEY; protect managed/local secrets, responses, logs and client bundles. +- verify-firestore (REASONED; firebase): Compare unsigned/A/B reads and writes to user documents and descendants, with a successful owner control; test parent documents to distinguish recursive-rule versions. +- verify-rtdb (REASONED; firebase): Compare unsigned/A/B reads/writes to seeded RTDB owner paths; ancestor grants expose them while ownership rules deny unauthorized callers. +- verify-firebase-storage (REASONED; firebase): Compare unsigned/A/B object reads and uploads; ownership rules must deny unrelated callers while owner operations succeed. +- verify-download-token (REASONED; firebase): Same sessionless download URL works after Rules tightening but must lose bytes after token revocation; owner getBytes remains successful and network/CORS failures are inconclusive. +- verify-tables (REASONED; supabase, postgres, curl): Pair unsigned/A/B profiles GETs against seeded rows, using ordinary credentials and required MFA; test intended writes and persisted effects separately. +- verify-views (REASONED; postgres): Compare view reads with table boundaries; owner-privileged exposure must close under invoker/revoked access while intended reads or deliberate removal are confirmed. +- verify-rpc (REASONED; supabase): Test every API function overload with valid harmless arguments as unsigned/A/B; unauthorized data or operations must be denied while the intended caller succeeds. +- verify-http (REASONED; supabase): Probe HTTP wrappers with a controlled collector and marker; unauthorized fixed calls must emit no request. Commit pg_net fixture transactions before expecting delivery. +- verify-vault (REASONED; supabase, postgres): Use a disposable Vault canary to compare exposed/fixed wrappers; inspect function and table privileges as supporting evidence. +- verify-storage (REASONED; supabase): Compare public URL and A/B private downloads plus intended uploads/upserts; unauthorized callers must receive no bytes or cross-owner writes. +- verify-realtime (REASONED; supabase): Compare A/B join, Broadcast and Presence delivery; public-disabled clients lacking private=true must fail, and connection alone is insufficient. +- verify-changes (REASONED; supabase): Observe A/B INSERT/UPDATE subscription events against seeded rows; fixed table authorization withholds unrelated events. Evaluate DELETE separately. +- verify-edge (REASONED; supabase): Compare no/invalid Bearer without apikey, authorized owners and valid webhook signatures; distinguish platform 401 from handler authorization and inspect effects/secrets. +- verify-firebase-functions (REASONED; firebase): Compare callable requests with valid App Check but missing user auth and A/B operations; HTTP endpoints need separate invalid/valid user or IAM comparisons. +- verify-appcheck (REASONED; firebase): Repeat owner requests with/without App Check per product/callable; enforcement must deny missing attestation while compatible clients work. +- verify-api-restrictions (REASONED; firebase): Test intended versus excluded apps/referrers after key restrictions, retaining successful intended product traffic. +- verify-emulators (REASONED; firebase): Enumerate hub /emulators and sockets, compare missing versus loaded Rules and second-host connectivity, and retain local owner controls. +- verify-firebase-accounts (REASONED; firebase): Sign out before each Anonymous signup test and check isNewUser; disabled providers must reject creation. Enumeration tests compare sign-in errors, not EMAIL_EXISTS signup behavior. +- verify-supabase-accounts (REASONED; supabase): Fresh signup and anonymous creation must fail under invite-only/anonymous-disabled settings while an invited account still signs in. +- verify-mfa (REASONED; supabase): Owner profiles GET with aal1 must yield no protected rows under the restrictive policy, while fresh aal2 returns the seeded owner row. +- verify-key-retirement (REASONED; supabase): Compare old/new API keys after explicit deactivation and old/new JWTs after revocation, accounting for propagation and caches. +- verify-api-removal (REASONED; supabase): Previously successful profiles requests must stop returning fixtures after schema removal or Data API disabling; retained schemas need authorized controls. +- verify-bundle (REASONED; supabase): Filename-only service_role/eyJ/sb_secret_ scanning yields review candidates, not proof of leaks; public anon JWTs are legitimate. Use a marker control and inspect encoded/split secrets separately. +- local-guards (DEMONSTRATED; supabase, shellcheck): Historical local guard tests rejected placeholders and malformed set lines; this guide's assessment is shell behavior only, not live authorization. The recorded ShellCheck 0.11.0 has a release citation, not host-binary evidence; bash and other tool/parser versions remain unrecorded. +- local-syntax (DEMONSTRATED; firebase, supabase, shellcheck): Recorded ShellCheck 0.11.0/bash and JSON/TOML/JavaScript parsing passed; this guide's assessment is that these checks do not compile Firebase Rules, execute SQL or demonstrate services. Only the ShellCheck release is cited; bash and other tool/parser versions remain unrecorded. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "6690687db9e1d66f3e67a898fc0b030022e55be24166bc29b8c35554e8081891", + "components": { + "firebase": { + "name": "Firebase documentation", + "basis": "unknown", + "sources": { + "s100c2cdbdd59": "https://firebase.google.com/docs/rules", + "s53bfe8b21868": "https://firebase.google.com/docs/rules/basics", + "s622d3bb0665b": "https://firebase.google.com/docs/firestore/quickstart", + "sb2c933c841ef": "https://firebase.google.com/docs/database/web/start", + "s75f018e0fac7": "https://firebase.google.com/support/releases", + "s7bd11c22c7ce": "https://firebase.google.com/docs/firestore/security/rules-structure", + "sa6b1c4933e6a": "https://firebase.google.com/docs/database/security/core-syntax", + "s0a7f54463abd": "https://firebase.google.com/docs/storage/security/core-syntax", + "sd6071548f6fe": "https://firebase.google.com/docs/firestore/security/get-started", + "s14872143a9e8": "https://firebase.google.com/docs/storage/security/get-started", + "sc8f214d059d2": "https://firebase.google.com/docs/functions/callable", + "s79da8f65f90a": "https://firebase.google.com/docs/functions/callable-reference", + "s9a86df9623a6": "https://firebase.google.com/docs/functions/1st-gen/callable-1st", + "s49f78d054708": "https://firebase.google.com/docs/auth/admin/verify-id-tokens", + "s1bb711c9afeb": "https://firebase.google.com/docs/functions/http-events", + "sc9f86ea271f6": "https://firebase.google.com/docs/reference/functions/2nd-gen/node/firebase-functions.https.httpsoptions", + "sb0ff8f1726f7": "https://firebase.google.com/docs/app-check/cloud-functions", + "sa71e958c99df": "https://firebase.google.com/docs/app-check/web/recaptcha-enterprise-provider", + "sba7129c83405": "https://firebase.google.com/docs/app-check/enable-enforcement", + "safeed4cd2b67": "https://firebase.google.com/docs/projects/api-keys", + "sc78d5eb59b65": "https://docs.cloud.google.com/api-keys/docs/add-restrictions-api-keys", + "sb0d26775f3b4": "https://firebase.google.com/docs/web/learn-more", + "s77e2cbd8a29a": "https://firebase.google.com/docs/auth/web/anonymous-auth", + "s06808e4b6965": "https://docs.cloud.google.com/identity-platform/docs/admin/email-enumeration-protection", + "sd8a1bda29f5c": "https://firebase.google.com/docs/emulator-suite/install_and_configure", + "s69d9721c7f29": "https://firebase.google.com/docs/emulator-suite/use_hosting", + "s8c90f9f6c7fa": "https://firebase.google.com/docs/reference/js/auth", + "s8c8336ec252e": "https://firebase.google.com/docs/reference/js/auth.additionaluserinfo", + "s142e56472093": "https://firebase.google.com/docs/auth/admin/manage-sessions", + "s9b7e57b472c7": "https://firebase.google.com/docs/storage/admin/start", + "sa0584b797732": "https://firebase.google.com/docs/reference/kotlin/com/google/firebase/storage/StorageReference", + "sfd1e7412abf6": "https://firebase.google.com/docs/storage/web/download-files" + } + }, + "supabase": { + "name": "Supabase documentation", + "basis": "unknown", + "sources": { + "s73d939b4644e": "https://supabase.com/docs/guides/api/api-keys", + "s46cff2590323": "https://supabase.com/docs/guides/database/postgres/row-level-security", + "s741468ff35a9": "https://supabase.com/docs/guides/auth/auth-mfa", + "s6d5cf2d4dbf3": "https://supabase.com/docs/guides/api/securing-your-api", + "s20ef046ac646": "https://supabase.com/docs/guides/api/using-custom-schemas", + "sa946354db383": "https://supabase.com/docs/guides/getting-started/migrating-to-new-api-keys", + "sfc2f9ae0232b": "https://supabase.com/docs/guides/auth/signing-keys", + "s8405a9fb4b74": "https://supabase.com/docs/guides/database/functions", + "s13da01bbbba5": "https://supabase.com/docs/guides/database/extensions/http", + "s76740bffcb32": "https://supabase.com/docs/guides/database/extensions/pg_net", + "s47bb8fa3e14e": "https://supabase.com/docs/guides/database/vault", + "s1567fb838f20": "https://supabase.com/docs/guides/storage/buckets/fundamentals", + "sdba7d7b6dcf8": "https://supabase.com/docs/guides/storage/security/access-control", + "s9e1484f46eff": "https://supabase.com/docs/guides/storage/schema/helper-functions", + "sc29febbb230e": "https://supabase.com/docs/guides/storage/serving/downloads", + "see2d3be99ebd": "https://supabase.com/docs/guides/storage/uploads/standard-uploads", + "s5674b1057e14": "https://supabase.com/docs/guides/realtime/settings", + "s84efad0ba39d": "https://supabase.com/docs/guides/realtime/authorization", + "s694eab64f2ec": "https://supabase.com/docs/guides/realtime/postgres-changes", + "sc797f0b63472": "https://supabase.com/docs/guides/auth/general-configuration", + "s3a5e39fe7b4d": "https://supabase.com/docs/guides/local-development/cli/config", + "sa2dd4637969e": "https://supabase.com/docs/guides/auth/passwords", + "sad79d4d1b6ee": "https://supabase.com/docs/guides/auth/auth-anonymous", + "s182755725d74": "https://supabase.com/docs/guides/auth/auth-mfa/totp", + "s92b405dc3c7e": "https://supabase.com/docs/reference/javascript/select", + "s087c8cf42ab0": "https://supabase.com/docs/reference/javascript/rpc", + "sf3246532dd18": "https://supabase.com/docs/guides/functions", + "s7dfbb47b5d20": "https://supabase.com/docs/reference/cli/supabase-functions-deploy", + "sf7d438b8c080": "https://supabase.com/docs/guides/functions/auth", + "se52bbd097033": "https://supabase.com/docs/guides/functions/auth-headers", + "sb3c4c0993e14": "https://supabase.com/docs/guides/functions/secrets", + "sb787b5d63633": "https://supabase.com/docs/guides/platform/network-restrictions", + "s83e15676e1ff": "https://supabase.com/docs/guides/platform/ssl-enforcement", + "sf69cafb4bc6c": "https://supabase.com/docs/guides/database/postgres/roles" + } + }, + "postgres": { + "name": "PostgreSQL documentation", + "basis": "unknown", + "sources": { + "s13c54ab9cc6b": "https://www.postgresql.org/docs/current/sql-createview.html", + "saa99ef28f60f": "https://www.postgresql.org/docs/current/sql-alterview.html", + "sb6224f4ac866": "https://www.postgresql.org/docs/current/sql-revoke.html", + "se9846041ce1f": "https://www.postgresql.org/docs/current/ddl-rowsecurity.html", + "s8e65a2f3f2bb": "https://www.postgresql.org/docs/current/sql-createpolicy.html", + "sec39c317b06f": "https://www.postgresql.org/docs/current/sql-createfunction.html", + "s0fe66d71aa10": "https://www.postgresql.org/docs/current/sql-alterfunction.html", + "s04e81888353f": "https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html", + "sc39eceeac711": "https://www.postgresql.org/docs/current/functions-info.html" + } + }, + "postgres-view": { + "name": "PostgreSQL invoker-view minimum", + "basis": "15", + "sources": { + "s14443e063ef1": "https://www.postgresql.org/docs/release/15.0/" + } + }, + "curl": { + "name": "curl", + "basis": "curl-8_14_1", + "sources": { + "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", + "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md" + } + }, + "shellcheck": { + "name": "ShellCheck", + "basis": "v0.11.0", + "sources": { + "sf994764bf270": "https://github.com/koalaman/shellcheck/blob/v0.11.0/CHANGELOG.md#L1" + } + } + }, + "claims": { + "public-config": {"text": "Firebase API keys/config and Supabase publishable/anon keys are public identifiers, not authorization; hosted endpoints handle TLS.", "components": ["firebase", "supabase"], "sources": ["firebase:safeed4cd2b67", "firebase:sb0d26775f3b4", "supabase:s73d939b4644e"], "status": "REASONED"}, + "firebase-ownership": {"text": "Require authentication and ownership separately in Firestore, RTDB and Storage; authentication-only rules do not isolate users.", "components": ["firebase"], "sources": ["firebase:s100c2cdbdd59", "firebase:s53bfe8b21868"], "status": "REASONED"}, + "firebase-starting": {"text": "Firestore production and RTDB locked modes deny clients; test templates expose data for about a month. Inspect expiry and replace open rules.", "components": ["firebase"], "sources": ["firebase:s622d3bb0665b", "firebase:sb2c933c841ef", "firebase:s75f018e0fac7"], "status": "REASONED"}, + "firestore-rules": {"text": "Rules version 2 recursive wildcards include the user document and descendants; version 1 excludes the parent. Unmatched paths deny and overlapping grants can reopen access.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, + "rtdb-rules": {"text": "Owner rules under users.$uid need ancestor grants removed; child rules cannot revoke parent grants or filter parent reads.", "components": ["firebase"], "sources": ["firebase:sa6b1c4933e6a"], "status": "REASONED"}, + "firebase-storage": {"text": "Storage has separate rules; conflicting default prose/example warrants explicit deny-all followed by deliberate owner-path access.", "components": ["firebase"], "sources": ["firebase:s53bfe8b21868", "firebase:s0a7f54463abd"], "status": "REASONED"}, + "firebase-download": {"text": "Storage download URLs are non-expiring bearer credentials unaffected by tighter Rules; revoke tokens and use authorized getBlob/getBytes direct downloads.", "components": ["firebase"], "sources": ["firebase:s9b7e57b472c7", "firebase:sa0584b797732", "firebase:sfd1e7412abf6"], "status": "REASONED"}, + "firebase-rule-deploy": {"text": "Inspect and test deployed rules for every database/bucket/instance; keep local and deployed definitions synchronized.", "components": ["firebase"], "sources": ["firebase:sd6071548f6fe", "firebase:s14872143a9e8"], "status": "REASONED"}, + "firebase-server": {"text": "Admin SDK/server credentials bypass client Rules; keep credentials server-side and apply IAM and caller authorization.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, + "firebase-callable": {"text": "Callable ID-token validation is optional authentication; reject missing request.auth, use its UID and validate resource access. First-generation handlers use context.auth.", "components": ["firebase"], "sources": ["firebase:s79da8f65f90a", "firebase:sc8f214d059d2", "firebase:s9a86df9623a6"], "status": "REASONED"}, + "firebase-callable-appcheck": {"text": "onCall enforceAppCheck needs firebase-functions 4.0.0 or newer; App Check does not replace caller/resource authorization.", "components": ["firebase"], "sources": ["firebase:sb0ff8f1726f7"], "status": "REASONED"}, + "firebase-http": {"text": "onRequest does not inherit callable auth; verify end-user tokens with verifyIdToken or restrict IAM invokers, then authorize the intended principal.", "components": ["firebase"], "sources": ["firebase:s49f78d054708", "firebase:s1bb711c9afeb", "firebase:sc9f86ea271f6"], "status": "REASONED"}, + "firebase-appcheck": {"text": "Register compatible clients and inspect metrics, then enforce App Check per product and per callable; registration alone enforces nothing.", "components": ["firebase"], "sources": ["firebase:sa71e958c99df", "firebase:sba7129c83405", "firebase:sb0ff8f1726f7"], "status": "REASONED"}, + "firebase-key-restrictions": {"text": "Apply one app-restriction type per key plus API restrictions; separate platforms and unrelated billable APIs, using the full Firebase product table.", "components": ["firebase"], "sources": ["firebase:sc78d5eb59b65", "firebase:safeed4cd2b67"], "status": "REASONED"}, + "firebase-anonymous": {"text": "Anonymous sign-in is opt-in; disable unused providers because anonymous accounts still satisfy authentication-only Rules.", "components": ["firebase"], "sources": ["firebase:s77e2cbd8a29a"], "status": "REASONED"}, + "firebase-enumeration": {"text": "Email enumeration protection defaults on only for projects created from September 15, 2023; signup can still return EMAIL_EXISTS.", "components": ["firebase"], "sources": ["firebase:s06808e4b6965"], "status": "REASONED"}, + "firebase-emulators": {"text": "Emulators default to localhost but missing Firestore/RTDB/Storage Rules configuration means open data; keep listeners, UI, hub, proxies and tunnels private.", "components": ["firebase"], "sources": ["firebase:sd8a1bda29f5c", "firebase:s69d9721c7f29"], "status": "REASONED"}, + "supabase-network": {"text": "Restrict hosted database/pooler client IPs with Network Restrictions, enforce SSL and protect database-role passwords.", "components": ["supabase"], "sources": ["supabase:sb787b5d63633", "supabase:s83e15676e1ff", "supabase:sf69cafb4bc6c"], "status": "REASONED"}, + "supabase-grants": {"text": "Data API access needs grants plus RLS; automatic-grant defaults are transitioning. Table Editor enables RLS, while SQL creation needs explicit enablement.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3"], "status": "REASONED"}, + "supabase-select": {"text": "The profiles baseline enables RLS, revokes broad grants and permits authenticated owners SELECT; audit inherited/column grants and existing policies.", "components": ["supabase", "postgres"], "sources": ["supabase:s46cff2590323", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, + "supabase-bypass": {"text": "With RLS enabled and no policy, ordinary subject roles get no rows; owners normally bypass, as do superusers/BYPASSRLS roles. TRUNCATE is outside RLS.", "components": ["postgres"], "sources": ["postgres:se9846041ce1f"], "status": "REASONED"}, + "supabase-writes": {"text": "Grant and policy INSERT/UPDATE/DELETE only when intended; UPDATE checks old/new ownership and needs applicable SELECT when reading columns.", "components": ["supabase", "postgres"], "sources": ["supabase:s46cff2590323", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, + "supabase-schemas": {"text": "public is exposed by default; restrict exposed schemas and deliberately grant schema/object access. Schema selection does not replace RLS.", "components": ["supabase"], "sources": ["supabase:s20ef046ac646"], "status": "REASONED"}, + "supabase-api-disable": {"text": "Disable an unused Data API to close generated REST endpoints; this does not disable all other services.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3"], "status": "REASONED"}, + "supabase-keys": {"text": "Publishable keys select anon or authenticated by session; secret and legacy service_role keys bypass RLS and belong only on trusted servers.", "components": ["supabase"], "sources": ["supabase:s73d939b4644e"], "status": "REASONED"}, + "supabase-key-retire": {"text": "Replacement keys do not disable legacy keys; migrate consumers then explicitly deactivate legacy keys, or delete compromised secret keys.", "components": ["supabase"], "sources": ["supabase:sa946354db383"], "status": "REASONED"}, + "supabase-jwt-retire": {"text": "Rotation changes the signing key but retains previous trust until revocation; account for verifier caches and propagation.", "components": ["supabase"], "sources": ["supabase:sfc2f9ae0232b"], "status": "REASONED"}, + "supabase-view-owner": {"text": "Views use owner privileges/policies by default; disclosure depends on reachable schemas, grants, query and owner privileges, not anon-key possession alone.", "components": ["postgres", "supabase"], "sources": ["postgres:s13c54ab9cc6b", "supabase:s46cff2590323"], "status": "REASONED"}, + "supabase-view-invoker": {"text": "PostgreSQL 15+ security_invoker views apply caller policies and require underlying privileges.", "components": ["postgres-view", "postgres"], "sources": ["postgres-view:s14443e063ef1", "postgres:saa99ef28f60f", "postgres:s13c54ab9cc6b"], "status": "REASONED"}, + "supabase-view-legacy": {"text": "PostgreSQL 14 and earlier require revoking all view privileges from PUBLIC/anon/authenticated or unexposing the schema; updatable views can bypass RLS for writes.", "components": ["postgres"], "sources": ["postgres:s13c54ab9cc6b", "postgres:sb6224f4ac866", "postgres:sc39eceeac711"], "status": "REASONED"}, + "supabase-functions": {"text": "Prefer INVOKER; restrict DEFINER execution, use empty search_path with qualified references and enforce caller authorization including required MFA.", "components": ["supabase", "postgres"], "sources": ["supabase:s8405a9fb4b74", "postgres:sec39c317b06f", "postgres:s0fe66d71aa10"], "status": "REASONED"}, + "supabase-function-defaults": {"text": "New functions grant PUBLIC execution; create privileged functions and restrict grants transactionally. Global/default revocations are creator-specific and do not fix existing functions.", "components": ["postgres"], "sources": ["postgres:sec39c317b06f", "postgres:s04e81888353f"], "status": "REASONED"}, + "supabase-http": {"text": "Audit http/pg_net wrappers and revoke unneeded execute grants; constrain callers/destinations/data. Extension enablement alone does not prove exploitable SSRF.", "components": ["supabase"], "sources": ["supabase:s13da01bbbba5", "supabase:s76740bffcb32"], "status": "REASONED"}, + "supabase-vault": {"text": "Restrict vault.decrypted_secrets and privileged wrappers and keep Vault unexposed; at-rest encryption cannot prevent authorized plaintext queries.", "components": ["supabase"], "sources": ["supabase:s47bb8fa3e14e"], "status": "REASONED"}, + "supabase-buckets": {"text": "Private downloads need authorized access or time-limited signed URLs; public buckets bypass retrieval checks but not upload/delete/move/copy controls.", "components": ["supabase"], "sources": ["supabase:s1567fb838f20"], "status": "REASONED"}, + "supabase-storage-policies": {"text": "Authorize storage.objects separately by bucket and owner path; upload needs INSERT, upsert also SELECT/UPDATE, and deletion needs deliberate permission.", "components": ["supabase"], "sources": ["supabase:sdba7d7b6dcf8", "supabase:s9e1484f46eff", "supabase:see2d3be99ebd"], "status": "REASONED"}, + "supabase-realtime-default": {"text": "Public Realtime channels are allowed by default; disable them project-wide and also set client config.private.", "components": ["supabase"], "sources": ["supabase:s5674b1057e14"], "status": "REASONED"}, + "supabase-realtime-policy": {"text": "Broadcast/Presence receive/send need separate SELECT/INSERT policies on realtime.messages, constrained by topic ownership and extension.", "components": ["supabase"], "sources": ["supabase:s84efad0ba39d"], "status": "REASONED"}, + "supabase-changes": {"text": "Postgres Changes uses source-table authorization; private-channel policies are not universal and DELETE has separate limitations.", "components": ["supabase"], "sources": ["supabase:s694eab64f2ec"], "status": "REASONED"}, + "supabase-signup": {"text": "Disable signup, anonymous sign-ins and unused providers when unwanted; inspect hosted settings instead of assuming universal defaults.", "components": ["supabase"], "sources": ["supabase:sc797f0b63472"], "status": "REASONED"}, + "supabase-cli-auth": {"text": "CLI enable_signup defaults true and enable_anonymous_sign_ins false; local config edits do not prove hosted settings changed.", "components": ["supabase"], "sources": ["supabase:s3a5e39fe7b4d"], "status": "REASONED"}, + "supabase-mfa": {"text": "TOTP is available on every plan at writing; enrollment alone does not constrain data. Require aal2 using a restrictive policy alongside permissive ownership policies.", "components": ["supabase", "postgres"], "sources": ["supabase:s182755725d74", "supabase:s741468ff35a9", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, + "supabase-edge-jwt": {"text": "Per-function verify_jwt defaults true; --no-verify-jwt disables platform checking and requires handler authentication before protected work.", "components": ["supabase"], "sources": ["supabase:s7dfbb47b5d20", "supabase:sf7d438b8c080", "supabase:se52bbd097033"], "status": "REASONED"}, + "supabase-edge-auth": {"text": "Platform JWT success is not resource authorization and API-key compatibility can pass it; separately authorize users or verify webhook signatures.", "components": ["supabase"], "sources": ["supabase:sf7d438b8c080", "supabase:se52bbd097033"], "status": "REASONED"}, + "supabase-edge-secrets": {"text": "Edge environments contain privileged credentials including SUPABASE_SERVICE_ROLE_KEY; protect managed/local secrets, responses, logs and client bundles.", "components": ["supabase"], "sources": ["supabase:sb3c4c0993e14"], "status": "REASONED"}, + "verify-firestore": {"text": "Compare unsigned/A/B reads and writes to user documents and descendants, with a successful owner control; test parent documents to distinguish recursive-rule versions.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, + "verify-rtdb": {"text": "Compare unsigned/A/B reads/writes to seeded RTDB owner paths; ancestor grants expose them while ownership rules deny unauthorized callers.", "components": ["firebase"], "sources": ["firebase:sa6b1c4933e6a"], "status": "REASONED"}, + "verify-firebase-storage": {"text": "Compare unsigned/A/B object reads and uploads; ownership rules must deny unrelated callers while owner operations succeed.", "components": ["firebase"], "sources": ["firebase:s0a7f54463abd"], "status": "REASONED"}, + "verify-download-token": {"text": "Same sessionless download URL works after Rules tightening but must lose bytes after token revocation; owner getBytes remains successful and network/CORS failures are inconclusive.", "components": ["firebase"], "sources": ["firebase:s9b7e57b472c7", "firebase:sa0584b797732", "firebase:sfd1e7412abf6"], "status": "REASONED"}, + "verify-tables": {"text": "Pair unsigned/A/B profiles GETs against seeded rows, using ordinary credentials and required MFA; test intended writes and persisted effects separately.", "components": ["supabase", "postgres", "curl"], "sources": ["supabase:s92b405dc3c7e", "postgres:se9846041ce1f", "supabase:s741468ff35a9", "curl:s5e1383feef7d", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, + "verify-views": {"text": "Compare view reads with table boundaries; owner-privileged exposure must close under invoker/revoked access while intended reads or deliberate removal are confirmed.", "components": ["postgres"], "sources": ["postgres:s13c54ab9cc6b"], "status": "REASONED"}, + "verify-rpc": {"text": "Test every API function overload with valid harmless arguments as unsigned/A/B; unauthorized data or operations must be denied while the intended caller succeeds.", "components": ["supabase"], "sources": ["supabase:s087c8cf42ab0", "supabase:s8405a9fb4b74"], "status": "REASONED"}, + "verify-http": {"text": "Probe HTTP wrappers with a controlled collector and marker; unauthorized fixed calls must emit no request. Commit pg_net fixture transactions before expecting delivery.", "components": ["supabase"], "sources": ["supabase:s76740bffcb32"], "status": "REASONED"}, + "verify-vault": {"text": "Use a disposable Vault canary to compare exposed/fixed wrappers; inspect function and table privileges as supporting evidence.", "components": ["supabase", "postgres"], "sources": ["supabase:s47bb8fa3e14e", "postgres:sc39eceeac711"], "status": "REASONED"}, + "verify-storage": {"text": "Compare public URL and A/B private downloads plus intended uploads/upserts; unauthorized callers must receive no bytes or cross-owner writes.", "components": ["supabase"], "sources": ["supabase:sc29febbb230e", "supabase:see2d3be99ebd", "supabase:s1567fb838f20"], "status": "REASONED"}, + "verify-realtime": {"text": "Compare A/B join, Broadcast and Presence delivery; public-disabled clients lacking private=true must fail, and connection alone is insufficient.", "components": ["supabase"], "sources": ["supabase:s5674b1057e14", "supabase:s84efad0ba39d"], "status": "REASONED"}, + "verify-changes": {"text": "Observe A/B INSERT/UPDATE subscription events against seeded rows; fixed table authorization withholds unrelated events. Evaluate DELETE separately.", "components": ["supabase"], "sources": ["supabase:s694eab64f2ec"], "status": "REASONED"}, + "verify-edge": {"text": "Compare no/invalid Bearer without apikey, authorized owners and valid webhook signatures; distinguish platform 401 from handler authorization and inspect effects/secrets.", "components": ["supabase"], "sources": ["supabase:se52bbd097033", "supabase:sf7d438b8c080", "supabase:sb3c4c0993e14"], "status": "REASONED"}, + "verify-firebase-functions": {"text": "Compare callable requests with valid App Check but missing user auth and A/B operations; HTTP endpoints need separate invalid/valid user or IAM comparisons.", "components": ["firebase"], "sources": ["firebase:s79da8f65f90a", "firebase:s49f78d054708"], "status": "REASONED"}, + "verify-appcheck": {"text": "Repeat owner requests with/without App Check per product/callable; enforcement must deny missing attestation while compatible clients work.", "components": ["firebase"], "sources": ["firebase:sba7129c83405", "firebase:sb0ff8f1726f7"], "status": "REASONED"}, + "verify-api-restrictions": {"text": "Test intended versus excluded apps/referrers after key restrictions, retaining successful intended product traffic.", "components": ["firebase"], "sources": ["firebase:safeed4cd2b67", "firebase:sc78d5eb59b65"], "status": "REASONED"}, + "verify-emulators": {"text": "Enumerate hub /emulators and sockets, compare missing versus loaded Rules and second-host connectivity, and retain local owner controls.", "components": ["firebase"], "sources": ["firebase:sd8a1bda29f5c", "firebase:s69d9721c7f29"], "status": "REASONED"}, + "verify-firebase-accounts": {"text": "Sign out before each Anonymous signup test and check isNewUser; disabled providers must reject creation. Enumeration tests compare sign-in errors, not EMAIL_EXISTS signup behavior.", "components": ["firebase"], "sources": ["firebase:s8c90f9f6c7fa", "firebase:s8c8336ec252e", "firebase:s142e56472093", "firebase:s77e2cbd8a29a", "firebase:s06808e4b6965"], "status": "REASONED"}, + "verify-supabase-accounts": {"text": "Fresh signup and anonymous creation must fail under invite-only/anonymous-disabled settings while an invited account still signs in.", "components": ["supabase"], "sources": ["supabase:sc797f0b63472", "supabase:sa2dd4637969e", "supabase:sad79d4d1b6ee"], "status": "REASONED"}, + "verify-mfa": {"text": "Owner profiles GET with aal1 must yield no protected rows under the restrictive policy, while fresh aal2 returns the seeded owner row.", "components": ["supabase"], "sources": ["supabase:s741468ff35a9"], "status": "REASONED", "verify": [1]}, + "verify-key-retirement": {"text": "Compare old/new API keys after explicit deactivation and old/new JWTs after revocation, accounting for propagation and caches.", "components": ["supabase"], "sources": ["supabase:sa946354db383", "supabase:sfc2f9ae0232b"], "status": "REASONED"}, + "verify-api-removal": {"text": "Previously successful profiles requests must stop returning fixtures after schema removal or Data API disabling; retained schemas need authorized controls.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3", "supabase:s20ef046ac646"], "status": "REASONED"}, + "verify-bundle": {"text": "Filename-only service_role/eyJ/sb_secret_ scanning yields review candidates, not proof of leaks; public anon JWTs are legitimate. Use a marker control and inspect encoded/split secrets separately.", "components": ["supabase"], "sources": ["supabase:s73d939b4644e"], "status": "REASONED", "verify": [2]}, + "local-guards": {"text": "Historical local guard tests rejected placeholders and malformed set lines; this guide's assessment is shell behavior only, not live authorization. The recorded ShellCheck 0.11.0 has a release citation, not host-binary evidence; bash and other tool/parser versions remain unrecorded.", "components": ["supabase", "shellcheck"], "sources": ["supabase:s92b405dc3c7e", "supabase:s73d939b4644e", "shellcheck:sf994764bf270"], "status": "DEMONSTRATED", "evidence": "Local guard tests refused embedded `REPLACE_WITH_` placeholders, `example.com`, angle brackets, empty values, and omitted or shortened `set --` lines."}, + "local-syntax": {"text": "Recorded ShellCheck 0.11.0/bash and JSON/TOML/JavaScript parsing passed; this guide's assessment is that these checks do not compile Firebase Rules, execute SQL or demonstrate services. Only the ShellCheck release is cited; bash and other tool/parser versions remain unrecorded.", "components": ["firebase", "supabase", "shellcheck"], "sources": ["firebase:sa6b1c4933e6a", "firebase:sd8a1bda29f5c", "supabase:s3a5e39fe7b4d", "firebase:sc8f214d059d2", "shellcheck:sf994764bf270"], "status": "DEMONSTRATED", "evidence": "The RTDB JSON, `firebase.json` fragment, Auth TOML, and callable JavaScript passed local parsing checks."} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/free-certificates.md b/plugin/skills/secure-deployment-config/references/free-certificates.md index 1c4f8c880..3299054d3 100644 --- a/plugin/skills/secure-deployment-config/references/free-certificates.md +++ b/plugin/skills/secure-deployment-config/references/free-certificates.md @@ -1,225 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-17", - "documentation_checked": "2026-09", - "body_sha256": "1f578874874b2d4b757f649adda8745f4d241949ddc0c71bfffb7315a7158257", - "components": { - "le": { - "name": "Let's Encrypt documentation", - "basis": "unknown", - "sources": { - "s9ff2d787c618": "https://letsencrypt.org/docs/", - "s01388c85c5d6": "https://letsencrypt.org/docs/cert-lifetimes/", - "s4406ef210fff": "https://letsencrypt.org/2025/12/02/from-90-to-45", - "s21efa5995c48": "https://letsencrypt.org/2025/08/06/ocsp-service-has-reached-end-of-life", - "saf908bcd2143": "https://letsencrypt.org/docs/caa/" - } - }, - "certbot": { - "name": "Certbot documentation", - "basis": "unknown", - "sources": { - "s44faaf4eb7d7": "https://certbot.eff.org/", - "s7b60a00cadf3": "https://eff-certbot.readthedocs.io/en/stable/using.html#manual", - "s875be163b4c1": "https://eff-certbot.readthedocs.io/en/stable/using.html#dns-plugins" - } - }, - "reconfigure": { - "name": "Certbot reconfigure minimum", - "basis": "2.3.0", - "sources": { - "s0dee7d6d65ba": "https://raw.githubusercontent.com/certbot/certbot/v2.3.0/certbot/docs/using.rst" - } - }, - "zerossl": { - "name": "ZeroSSL", - "basis": "unknown", - "sources": { - "sddca28a3381b": "https://zerossl.com/" - } - }, - "acme": { - "name": "acme.sh", - "basis": "unknown", - "sources": { - "sa635c50fc1f9": "https://github.com/acmesh-official/acme.sh" - } - }, - "caddy": { - "name": "Caddy automatic HTTPS", - "basis": "v2.11.4", - "sources": { - "s9683cb8d689f": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L62-L63", - "s409f9221aec2": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L280-L296", - "s80dbf0751153": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L245-L249", - "s5216789c9341": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L321-L328" - } - }, - "certmagic": { - "name": "certmagic ACME implementation", - "basis": "v0.25.3", - "sources": { - "s61e3cba719df": "https://github.com/caddyserver/caddy/blob/v2.11.4/go.mod#L12-L12", - "s8782e006523b": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeclient.go#L188-L226", - "s465c4ddcc92f": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeclient.go#L304-L313", - "sc1e2ddefe5ed": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L116-L120", - "sd423f53faa1a": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L222-L236", - "sf2fbc79f6542": "https://github.com/caddyserver/certmagic/blob/v0.25.3/certmagic.go#L473-L475", - "s1c3236df58b9": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L647-L654" - } - }, - "traefik": { - "name": "Traefik ACME documentation", - "basis": "v3.5", - "sources": { - "s418a6b1ad09b": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/tls/certificate-resolvers/acme/" - } - }, - "openssl-cli": { - "name": "OpenSSL certificate commands", - "basis": "3.0", - "sources": { - "s00aaf106164f": "https://docs.openssl.org/3.0/man1/openssl-s_client/", - "s0856f4a58574": "https://docs.openssl.org/3.0/man1/openssl-x509/" - } - }, - "curl-source": { - "name": "curl command documentation", - "basis": "curl-8_12_1", - "sources": { - "s76f69702c9d5": "https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/head.md#L21-L22", - "s60820cd231e1": "https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/insecure.md#L21-L28" - } - }, - "certbot-v230": { - "name": "Certbot user guide and source", - "basis": "v2.3.0", - "sources": { - "s725109195132": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L808-L825", - "s2f25bc233392": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L632-L640", - "s11c7994242fc": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L704-L710", - "sbb0c60d635ee": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L684", - "s55ec19f37395": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L194-L220", - "s5bf4ff9fbe76": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L222-L265", - "s7a053488f705": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/cli_utils.py#L227-L231", - "sa18c858c4e49": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/renewal.py#L40-L53", - "s2e4bd79a7795": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/renewal.py#L509-L534", - "s1910987bfd8c": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L295-L312", - "s9c6118b72e0a": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L1120-L1128", - "s3e22e32a2862": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L1243-L1249" - } - }, - "certbot-dns-cloudflare-v230": { - "name": "Certbot Cloudflare DNS plugin", - "basis": "v2.3.0", - "sources": { - "s3f07ef3d544d": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py#L24-L39", - "sc3b33fa2c90f": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py#L61-L78" - } - }, - "cloudflare-origin-ca": { - "name": "Cloudflare Origin CA documentation", - "basis": "checked October 2026", - "sources": { - "s1e15de5c9f36": "https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/" - } - } - }, - "claims": { - "public-trust": {"text": "ACME CAs such as Let's Encrypt and ZeroSSL issue free publicly trusted certificates; this guide advises preferring them for public DNS names, including private hosts using DNS-01.", "components": ["le", "zerossl"], "sources": ["le:s9ff2d787c618", "zerossl:sddca28a3381b"], "status": "REASONED"}, - "http-challenge": {"text": "HTTP-01 needs a public A/AAAA or CNAME pointing to the server and inbound internet access to port 80.", "components": ["le"], "sources": ["le:s9ff2d787c618"], "status": "REASONED"}, - "alpn-challenge": {"text": "TLS-ALPN-01 needs a public DNS record and inbound port 443; Caddy's ACME issuer enables the TLS-ALPN-01 solver when DNS-01 is not configured and TLS-ALPN-01 is not disabled, and can use an alternate local port if port 443 forwards to it; Traefik also uses TLS-ALPN-01.", "components": ["le", "caddy", "certmagic", "traefik"], "sources": ["le:s9ff2d787c618", "caddy:s409f9221aec2", "caddy:s80dbf0751153", "caddy:s5216789c9341", "certmagic:s61e3cba719df", "certmagic:s8782e006523b", "certmagic:s465c4ddcc92f", "certmagic:sc1e2ddefe5ed", "certmagic:sd423f53faa1a", "certmagic:sf2fbc79f6542", "certmagic:s1c3236df58b9", "traefik:s418a6b1ad09b"], "status": "REASONED"}, - "dns-challenge": {"text": "DNS-01 needs control of the challenge TXT record, no public address record or inbound port, and supports wildcards; automated issuance and renewal need an automated way to update TXT records, and this guide advises a provider DNS plugin and scoped API credentials, while Certbot also supports manual DNS-01 and an authentication hook for automatic renewal.", "components": ["le", "certbot", "certbot-v230"], "sources": ["le:s9ff2d787c618", "certbot:s7b60a00cadf3", "certbot:s875be163b4c1", "certbot-v230:s55ec19f37395", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"}, - "certbot-install": {"text": "Install Certbot and nginx/Apache plugins from the distribution, or the classic snap with /usr/bin/certbot pointing to /snap/bin/certbot.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, - "server-plugins": {"text": "certbot --nginx or --apache issues and installs certificates for the supplied domain names when the server is supported.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, - "standalone": {"text": "certonly --standalone binds port 80; stop its current owner and arrange webroot, a server plugin or port-freeing hooks for unattended renewal.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, - "webroot": {"text": "certonly --webroot uses /var/www/html to serve challenge files while the existing web server keeps running.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, - "dns-credentials": {"text": "The Cloudflare DNS plugin uses a credentials file and a Zone:DNS:Edit token limited to required zones; this guide requires never using the Global API Key and protecting the file with mode 600 in a 700 directory.", "components": ["certbot", "certbot-dns-cloudflare-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-dns-cloudflare-v230:s3f07ef3d544d", "certbot-dns-cloudflare-v230:sc3b33fa2c90f"], "status": "REASONED"}, - "certificate-paths": {"text": "Configure servers against stable /etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem paths for the chain and private key.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, - "renewal-schedule": {"text": "The Certbot v2.3.0 user guide says most installations have a scheduled task running certbot renew and says that, on Linux and BSD, you can check cron or systemd timers if unsure; this guide requires confirming the task and testing renewal plus reload with --dry-run --run-deploy-hooks, including package and snap installations.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-v230:s725109195132"], "status": "REASONED"}, - "deploy-hook": {"text": "This guide requires persisting a reload hook during successful initial issuance or successful renewal, or installing an executable in renewal-hooks/deploy; Certbot v2.3.0 saves the renewal-relevant options set on the command line to a lineage after successful renewal, and a renew --deploy-hook invocation with no renewal due does not save that hook.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-v230:s2f25bc233392", "certbot-v230:s11c7994242fc", "certbot-v230:sbb0c60d635ee", "certbot-v230:s7a053488f705", "certbot-v230:sa18c858c4e49", "certbot-v230:s2e4bd79a7795", "certbot-v230:s1910987bfd8c", "certbot-v230:s9c6118b72e0a", "certbot-v230:s3e22e32a2862"], "status": "REASONED"}, - "reconfigure": {"text": "certbot reconfigure can add the reload hook later on Certbot 2.3.0 and later.", "components": ["reconfigure"], "sources": ["reconfigure:s0dee7d6d65ba"], "status": "REASONED"}, - "expiry-monitoring": {"text": "Monitor the served certificate's expiry and renewal failures instead of relying on CA reminder emails; include a dry run in the deployment checklist. This guide assesses that any renewal step involving a person will fail at those scheduled shorter certificate lifetimes and that unattended automation is the only viable path.", "components": ["le", "certbot"], "sources": ["le:s4406ef210fff", "certbot:s44faaf4eb7d7"], "status": "REASONED"}, - "default-lifetime": {"text": "At the guide's September 2026 documentation check, Let's Encrypt's default certificate lifetime is 90 days.", "components": ["le"], "sources": ["le:s01388c85c5d6"], "status": "REASONED"}, - "short-lifetime": {"text": "Optional 6-day short-lived certificates are available to every subscriber as of the guide's September 2026 check.", "components": ["le"], "sources": ["le:s01388c85c5d6"], "status": "REASONED"}, - "lifetime-schedule": {"text": "The classic profile moves to 64 days on 2027-02-10 and 45 days on 2028-02-16; industry rules cap publicly trusted certificates at 47 days from 2029-03-15.", "components": ["le"], "sources": ["le:s01388c85c5d6", "le:s4406ef210fff"], "status": "REASONED"}, - "revocation": {"text": "Let's Encrypt ended OCSP on 2025-08-06 and publishes revocation through CRLs only; do not add OCSP stapling directives for its certificates.", "components": ["le"], "sources": ["le:s21efa5995c48"], "status": "REASONED"}, - "staging": {"text": "Let's Encrypt limits per-domain issuance; test with certbot --staging or --test-cert before real issuance.", "components": ["le", "certbot"], "sources": ["le:s9ff2d787c618", "certbot:s44faaf4eb7d7"], "status": "REASONED"}, - "caa": {"text": "CAA restricts issuing CAs, using issue letsencrypt.org for Let's Encrypt; certificate-transparency monitoring detects mis-issuance afterwards and complements CAA.", "components": ["le"], "sources": ["le:saf908bcd2143", "le:s9ff2d787c618"], "status": "REASONED"}, - "zerossl-eab": {"text": "ZeroSSL offers free ACME certificates; some clients need dashboard EAB credentials.", "components": ["zerossl"], "sources": ["zerossl:sddca28a3381b"], "status": "REASONED"}, - "acme-default": {"text": "acme.sh registers with ZeroSSL by default.", "components": ["acme"], "sources": ["acme:sa635c50fc1f9"], "status": "REASONED"}, - "acme-install": {"text": "acme.sh is a shell ACME client supporting many DNS providers; the guide recommends installation from a repository release instead of piping a download into a shell.", "components": ["acme"], "sources": ["acme:sa635c50fc1f9"], "status": "REASONED"}, - "server-automation": {"text": "This guide advises Caddy or Traefik for built-in issuance and renewal without an external client; it assesses them as the lowest-effort correct option for new deployments.", "components": ["le", "caddy", "traefik"], "sources": ["le:s9ff2d787c618", "caddy:s9683cb8d689f", "traefik:s418a6b1ad09b"], "status": "REASONED"}, - "origin-certificates": {"text": "Cloudflare lists Origin CA certificates on its Free plan, refers to long-lived origin CA certificates and says the validity period is chosen at creation, says they only encrypt traffic between Cloudflare and the origin, and warns that visitors may see untrusted certificate errors when Cloudflare is paused or proxying is disabled; this guide requires using them only for origins that receive traffic only through Cloudflare-proxied records.", "components": ["le", "cloudflare-origin-ca"], "sources": ["le:s9ff2d787c618", "cloudflare-origin-ca:s1e15de5c9f36"], "status": "REASONED"}, - "verify-inventory": {"text": "certbot certificates lists issued certificates and their expiry dates.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED", "verify": [1]}, - "verify-https": {"text": "Use the deployment's public hostname rather than example.com; curl HTTPS should succeed without -k.", "components": ["le", "curl-source"], "sources": ["le:s9ff2d787c618", "curl-source:s76f69702c9d5", "curl-source:s60820cd231e1"], "status": "REASONED", "verify": [1]}, - "verify-certificate": {"text": "The OpenSSL probe targets the hostname on port 443 with SNI, hostname verification and verification errors enabled, then displays issuer and dates.", "components": ["le", "openssl-cli"], "sources": ["le:s9ff2d787c618", "openssl-cli:s00aaf106164f", "openssl-cli:s0856f4a58574"], "status": "REASONED", "verify": [1]}, - "authentication": {"text": "A certificate alone is insufficient protection; continue with server controls and authentication.", "components": ["le"], "sources": ["le:s9ff2d787c618"], "status": "REASONED"}, - "dns-automation": {"text": "For automated DNS-01 issuance and renewal, this guide advises using a provider DNS plugin and least-privilege API credentials; Certbot also documents manual TXT entry and authentication hooks that automate the challenge.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s875be163b4c1", "certbot:s7b60a00cadf3", "certbot-v230:s55ec19f37395", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"}, - "dns-manual": {"text": "certbot certonly --manual --preferred-challenges dns permits hand-created TXT records; the Certbot v2.3.0 guide says certificates created with --manual do not support automatic renewal without a --manual-auth-hook that automates the challenge.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s7b60a00cadf3", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"} - } -} ---- # Free publicly trusted certificates (ACME) - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +Written against Let's Encrypt documentation (no recorded version), Certbot reconfigure minimum 2.3.0, Certbot user guide and source v2.3.0, Certbot Cloudflare DNS plugin v2.3.0, Caddy automatic HTTPS v2.11.4, certmagic ACME implementation v0.25.3, and Traefik ACME documentation v3.5, plus 3 more recorded bases; 3 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 31 claims, all reasoned from cited sources. -Metadata reviewed 2026-10-17; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- le: Let's Encrypt documentation (unknown) -- certbot: Certbot documentation (unknown) -- reconfigure: Certbot reconfigure minimum (2.3.0) -- zerossl: ZeroSSL (unknown) -- acme: acme.sh (unknown) -- caddy: Caddy automatic HTTPS (v2.11.4) -- certmagic: certmagic ACME implementation (v0.25.3) -- traefik: Traefik ACME documentation (v3.5) -- openssl-cli: OpenSSL certificate commands (3.0) -- curl-source: curl command documentation (curl-8_12_1) -- certbot-v230: Certbot user guide and source (v2.3.0) -- certbot-dns-cloudflare-v230: Certbot Cloudflare DNS plugin (v2.3.0) -- cloudflare-origin-ca: Cloudflare Origin CA documentation (checked October 2026) - -Claims (full source mapping in the front matter): - -- public-trust (REASONED; le, zerossl): ACME CAs such as Let's Encrypt and ZeroSSL issue free publicly trusted certificates; this guide advises preferring them for public DNS names, including private hosts using DNS-01. -- http-challenge (REASONED; le): HTTP-01 needs a public A/AAAA or CNAME pointing to the server and inbound internet access to port 80. -- alpn-challenge (REASONED; le, caddy, certmagic, traefik): TLS-ALPN-01 needs a public DNS record and inbound port 443; Caddy's ACME issuer enables the TLS-ALPN-01 solver when DNS-01 is not configured and TLS-ALPN-01 is not disabled, and can use an alternate local port if port 443 forwards to it; Traefik also uses TLS-ALPN-01. -- dns-challenge (REASONED; le, certbot, certbot-v230): DNS-01 needs control of the challenge TXT record, no public address record or inbound port, and supports wildcards; automated issuance and renewal need an automated way to update TXT records, and this guide advises a provider DNS plugin and scoped API credentials, while Certbot also supports manual DNS-01 and an authentication hook for automatic renewal. -- certbot-install (REASONED; certbot): Install Certbot and nginx/Apache plugins from the distribution, or the classic snap with /usr/bin/certbot pointing to /snap/bin/certbot. -- server-plugins (REASONED; certbot): certbot --nginx or --apache issues and installs certificates for the supplied domain names when the server is supported. -- standalone (REASONED; certbot): certonly --standalone binds port 80; stop its current owner and arrange webroot, a server plugin or port-freeing hooks for unattended renewal. -- webroot (REASONED; certbot): certonly --webroot uses /var/www/html to serve challenge files while the existing web server keeps running. -- dns-credentials (REASONED; certbot, certbot-dns-cloudflare-v230): The Cloudflare DNS plugin uses a credentials file and a Zone:DNS:Edit token limited to required zones; this guide requires never using the Global API Key and protecting the file with mode 600 in a 700 directory. -- certificate-paths (REASONED; certbot): Configure servers against stable /etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem paths for the chain and private key. -- renewal-schedule (REASONED; certbot, certbot-v230): The Certbot v2.3.0 user guide says most installations have a scheduled task running certbot renew and says that, on Linux and BSD, you can check cron or systemd timers if unsure; this guide requires confirming the task and testing renewal plus reload with --dry-run --run-deploy-hooks, including package and snap installations. -- deploy-hook (REASONED; certbot, certbot-v230): This guide requires persisting a reload hook during successful initial issuance or successful renewal, or installing an executable in renewal-hooks/deploy; Certbot v2.3.0 saves the renewal-relevant options set on the command line to a lineage after successful renewal, and a renew --deploy-hook invocation with no renewal due does not save that hook. -- reconfigure (REASONED; reconfigure): certbot reconfigure can add the reload hook later on Certbot 2.3.0 and later. -- expiry-monitoring (REASONED; le, certbot): Monitor the served certificate's expiry and renewal failures instead of relying on CA reminder emails; include a dry run in the deployment checklist. This guide assesses that any renewal step involving a person will fail at those scheduled shorter certificate lifetimes and that unattended automation is the only viable path. -- default-lifetime (REASONED; le): At the guide's September 2026 documentation check, Let's Encrypt's default certificate lifetime is 90 days. -- short-lifetime (REASONED; le): Optional 6-day short-lived certificates are available to every subscriber as of the guide's September 2026 check. -- lifetime-schedule (REASONED; le): The classic profile moves to 64 days on 2027-02-10 and 45 days on 2028-02-16; industry rules cap publicly trusted certificates at 47 days from 2029-03-15. -- revocation (REASONED; le): Let's Encrypt ended OCSP on 2025-08-06 and publishes revocation through CRLs only; do not add OCSP stapling directives for its certificates. -- staging (REASONED; le, certbot): Let's Encrypt limits per-domain issuance; test with certbot --staging or --test-cert before real issuance. -- caa (REASONED; le): CAA restricts issuing CAs, using issue letsencrypt.org for Let's Encrypt; certificate-transparency monitoring detects mis-issuance afterwards and complements CAA. -- zerossl-eab (REASONED; zerossl): ZeroSSL offers free ACME certificates; some clients need dashboard EAB credentials. -- acme-default (REASONED; acme): acme.sh registers with ZeroSSL by default. -- acme-install (REASONED; acme): acme.sh is a shell ACME client supporting many DNS providers; the guide recommends installation from a repository release instead of piping a download into a shell. -- server-automation (REASONED; le, caddy, traefik): This guide advises Caddy or Traefik for built-in issuance and renewal without an external client; it assesses them as the lowest-effort correct option for new deployments. -- origin-certificates (REASONED; le, cloudflare-origin-ca): Cloudflare lists Origin CA certificates on its Free plan, refers to long-lived origin CA certificates and says the validity period is chosen at creation, says they only encrypt traffic between Cloudflare and the origin, and warns that visitors may see untrusted certificate errors when Cloudflare is paused or proxying is disabled; this guide requires using them only for origins that receive traffic only through Cloudflare-proxied records. -- verify-inventory (REASONED; certbot): certbot certificates lists issued certificates and their expiry dates. -- verify-https (REASONED; le, curl-source): Use the deployment's public hostname rather than example.com; curl HTTPS should succeed without -k. -- verify-certificate (REASONED; le, openssl-cli): The OpenSSL probe targets the hostname on port 443 with SNI, hostname verification and verification errors enabled, then displays issuer and dates. -- authentication (REASONED; le): A certificate alone is insufficient protection; continue with server controls and authentication. -- dns-automation (REASONED; certbot, certbot-v230): For automated DNS-01 issuance and renewal, this guide advises using a provider DNS plugin and least-privilege API credentials; Certbot also documents manual TXT entry and authentication hooks that automate the challenge. -- dns-manual (REASONED; certbot, certbot-v230): certbot certonly --manual --preferred-challenges dns permits hand-created TXT records; the Certbot v2.3.0 guide says certificates created with --manual do not support automatic renewal without a --manual-auth-hook that automates the challenge. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Publicly trusted certificates are free through ACME certificate authorities such as Let's Encrypt and ZeroSSL. Browsers and libraries accept them without any client-side configuration, which is why this guide assesses them as the correct choice for every service with a public DNS name. Use [self-signed.md](self-signed.md) only when no public domain exists; a host that cannot accept inbound connections can still get a publicly trusted certificate through the DNS-01 challenge (below), or serve behind [cloudflare.md](cloudflare.md). @@ -364,3 +147,246 @@ REASONED: preceding block; certificate inventory, HTTPS access and served-certif - Certbot v2.3.0 Cloudflare DNS credentials (checked October 2026): - Certbot v2.3.0 Cloudflare DNS credential permissions (checked October 2026): - Cloudflare Origin CA (checked October 2026): https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/ + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/free-certificates.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- le: Let's Encrypt documentation (unknown) +- zerossl: ZeroSSL (unknown) +- certbot: Certbot documentation (unknown) +- reconfigure: Certbot reconfigure minimum (2.3.0) +- acme: acme.sh (unknown) +- certbot-v230: Certbot user guide and source (v2.3.0) +- certbot-dns-cloudflare-v230: Certbot Cloudflare DNS plugin (v2.3.0) +- caddy: Caddy automatic HTTPS (v2.11.4) +- certmagic: certmagic ACME implementation (v0.25.3) +- traefik: Traefik ACME documentation (v3.5) +- cloudflare-origin-ca: Cloudflare Origin CA documentation (checked October 2026) +- openssl-cli: OpenSSL certificate commands (3.0) +- curl-source: curl command documentation (curl-8_12_1) + +Claims (full source mapping in the JSON block below): + +- public-trust (REASONED; le, zerossl): ACME CAs such as Let's Encrypt and ZeroSSL issue free publicly trusted certificates; this guide advises preferring them for public DNS names, including private hosts using DNS-01. +- http-challenge (REASONED; le): HTTP-01 needs a public A/AAAA or CNAME pointing to the server and inbound internet access to port 80. +- alpn-challenge (REASONED; le, caddy, certmagic, traefik): TLS-ALPN-01 needs a public DNS record and inbound port 443; Caddy's ACME issuer enables the TLS-ALPN-01 solver when DNS-01 is not configured and TLS-ALPN-01 is not disabled, and can use an alternate local port if port 443 forwards to it; Traefik also uses TLS-ALPN-01. +- dns-challenge (REASONED; le, certbot, certbot-v230): DNS-01 needs control of the challenge TXT record, no public address record or inbound port, and supports wildcards; automated issuance and renewal need an automated way to update TXT records, and this guide advises a provider DNS plugin and scoped API credentials, while Certbot also supports manual DNS-01 and an authentication hook for automatic renewal. +- certbot-install (REASONED; certbot): Install Certbot and nginx/Apache plugins from the distribution, or the classic snap with /usr/bin/certbot pointing to /snap/bin/certbot. +- server-plugins (REASONED; certbot): certbot --nginx or --apache issues and installs certificates for the supplied domain names when the server is supported. +- standalone (REASONED; certbot): certonly --standalone binds port 80; stop its current owner and arrange webroot, a server plugin or port-freeing hooks for unattended renewal. +- webroot (REASONED; certbot): certonly --webroot uses /var/www/html to serve challenge files while the existing web server keeps running. +- dns-credentials (REASONED; certbot, certbot-dns-cloudflare-v230): The Cloudflare DNS plugin uses a credentials file and a Zone:DNS:Edit token limited to required zones; this guide requires never using the Global API Key and protecting the file with mode 600 in a 700 directory. +- certificate-paths (REASONED; certbot): Configure servers against stable /etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem paths for the chain and private key. +- renewal-schedule (REASONED; certbot, certbot-v230): The Certbot v2.3.0 user guide says most installations have a scheduled task running certbot renew and says that, on Linux and BSD, you can check cron or systemd timers if unsure; this guide requires confirming the task and testing renewal plus reload with --dry-run --run-deploy-hooks, including package and snap installations. +- deploy-hook (REASONED; certbot, certbot-v230): This guide requires persisting a reload hook during successful initial issuance or successful renewal, or installing an executable in renewal-hooks/deploy; Certbot v2.3.0 saves the renewal-relevant options set on the command line to a lineage after successful renewal, and a renew --deploy-hook invocation with no renewal due does not save that hook. +- reconfigure (REASONED; reconfigure): certbot reconfigure can add the reload hook later on Certbot 2.3.0 and later. +- expiry-monitoring (REASONED; le, certbot): Monitor the served certificate's expiry and renewal failures instead of relying on CA reminder emails; include a dry run in the deployment checklist. This guide assesses that any renewal step involving a person will fail at those scheduled shorter certificate lifetimes and that unattended automation is the only viable path. +- default-lifetime (REASONED; le): At the guide's September 2026 documentation check, Let's Encrypt's default certificate lifetime is 90 days. +- short-lifetime (REASONED; le): Optional 6-day short-lived certificates are available to every subscriber as of the guide's September 2026 check. +- lifetime-schedule (REASONED; le): The classic profile moves to 64 days on 2027-02-10 and 45 days on 2028-02-16; industry rules cap publicly trusted certificates at 47 days from 2029-03-15. +- revocation (REASONED; le): Let's Encrypt ended OCSP on 2025-08-06 and publishes revocation through CRLs only; do not add OCSP stapling directives for its certificates. +- staging (REASONED; le, certbot): Let's Encrypt limits per-domain issuance; test with certbot --staging or --test-cert before real issuance. +- caa (REASONED; le): CAA restricts issuing CAs, using issue letsencrypt.org for Let's Encrypt; certificate-transparency monitoring detects mis-issuance afterwards and complements CAA. +- zerossl-eab (REASONED; zerossl): ZeroSSL offers free ACME certificates; some clients need dashboard EAB credentials. +- acme-default (REASONED; acme): acme.sh registers with ZeroSSL by default. +- acme-install (REASONED; acme): acme.sh is a shell ACME client supporting many DNS providers; the guide recommends installation from a repository release instead of piping a download into a shell. +- server-automation (REASONED; le, caddy, traefik): This guide advises Caddy or Traefik for built-in issuance and renewal without an external client; it assesses them as the lowest-effort correct option for new deployments. +- origin-certificates (REASONED; le, cloudflare-origin-ca): Cloudflare lists Origin CA certificates on its Free plan, refers to long-lived origin CA certificates and says the validity period is chosen at creation, says they only encrypt traffic between Cloudflare and the origin, and warns that visitors may see untrusted certificate errors when Cloudflare is paused or proxying is disabled; this guide requires using them only for origins that receive traffic only through Cloudflare-proxied records. +- verify-inventory (REASONED; certbot): certbot certificates lists issued certificates and their expiry dates. +- verify-https (REASONED; le, curl-source): Use the deployment's public hostname rather than example.com; curl HTTPS should succeed without -k. +- verify-certificate (REASONED; le, openssl-cli): The OpenSSL probe targets the hostname on port 443 with SNI, hostname verification and verification errors enabled, then displays issuer and dates. +- authentication (REASONED; le): A certificate alone is insufficient protection; continue with server controls and authentication. +- dns-automation (REASONED; certbot, certbot-v230): For automated DNS-01 issuance and renewal, this guide advises using a provider DNS plugin and least-privilege API credentials; Certbot also documents manual TXT entry and authentication hooks that automate the challenge. +- dns-manual (REASONED; certbot, certbot-v230): certbot certonly --manual --preferred-challenges dns permits hand-created TXT records; the Certbot v2.3.0 guide says certificates created with --manual do not support automatic renewal without a --manual-auth-hook that automates the challenge. + +````json +{ + "schema": 1, + "checked": "2026-10-17", + "documentation_checked": "2026-09", + "body_sha256": "1f578874874b2d4b757f649adda8745f4d241949ddc0c71bfffb7315a7158257", + "components": { + "le": { + "name": "Let's Encrypt documentation", + "basis": "unknown", + "sources": { + "s9ff2d787c618": "https://letsencrypt.org/docs/", + "s01388c85c5d6": "https://letsencrypt.org/docs/cert-lifetimes/", + "s4406ef210fff": "https://letsencrypt.org/2025/12/02/from-90-to-45", + "s21efa5995c48": "https://letsencrypt.org/2025/08/06/ocsp-service-has-reached-end-of-life", + "saf908bcd2143": "https://letsencrypt.org/docs/caa/" + } + }, + "zerossl": { + "name": "ZeroSSL", + "basis": "unknown", + "sources": { + "sddca28a3381b": "https://zerossl.com/" + } + }, + "certbot": { + "name": "Certbot documentation", + "basis": "unknown", + "sources": { + "s44faaf4eb7d7": "https://certbot.eff.org/", + "s7b60a00cadf3": "https://eff-certbot.readthedocs.io/en/stable/using.html#manual", + "s875be163b4c1": "https://eff-certbot.readthedocs.io/en/stable/using.html#dns-plugins" + } + }, + "reconfigure": { + "name": "Certbot reconfigure minimum", + "basis": "2.3.0", + "sources": { + "s0dee7d6d65ba": "https://raw.githubusercontent.com/certbot/certbot/v2.3.0/certbot/docs/using.rst" + } + }, + "acme": { + "name": "acme.sh", + "basis": "unknown", + "sources": { + "sa635c50fc1f9": "https://github.com/acmesh-official/acme.sh" + } + }, + "certbot-v230": { + "name": "Certbot user guide and source", + "basis": "v2.3.0", + "sources": { + "s725109195132": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L808-L825", + "s2f25bc233392": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L632-L640", + "s11c7994242fc": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L704-L710", + "sbb0c60d635ee": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L684", + "s55ec19f37395": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L194-L220", + "s5bf4ff9fbe76": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L222-L265", + "s7a053488f705": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/cli_utils.py#L227-L231", + "sa18c858c4e49": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/renewal.py#L40-L53", + "s2e4bd79a7795": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/renewal.py#L509-L534", + "s1910987bfd8c": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L295-L312", + "s9c6118b72e0a": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L1120-L1128", + "s3e22e32a2862": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L1243-L1249" + } + }, + "certbot-dns-cloudflare-v230": { + "name": "Certbot Cloudflare DNS plugin", + "basis": "v2.3.0", + "sources": { + "s3f07ef3d544d": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py#L24-L39", + "sc3b33fa2c90f": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py#L61-L78" + } + }, + "caddy": { + "name": "Caddy automatic HTTPS", + "basis": "v2.11.4", + "sources": { + "s9683cb8d689f": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L62-L63", + "s409f9221aec2": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L280-L296", + "s80dbf0751153": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L245-L249", + "s5216789c9341": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L321-L328" + } + }, + "certmagic": { + "name": "certmagic ACME implementation", + "basis": "v0.25.3", + "sources": { + "s61e3cba719df": "https://github.com/caddyserver/caddy/blob/v2.11.4/go.mod#L12-L12", + "s8782e006523b": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeclient.go#L188-L226", + "s465c4ddcc92f": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeclient.go#L304-L313", + "sc1e2ddefe5ed": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L116-L120", + "sd423f53faa1a": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L222-L236", + "sf2fbc79f6542": "https://github.com/caddyserver/certmagic/blob/v0.25.3/certmagic.go#L473-L475", + "s1c3236df58b9": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L647-L654" + } + }, + "traefik": { + "name": "Traefik ACME documentation", + "basis": "v3.5", + "sources": { + "s418a6b1ad09b": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/tls/certificate-resolvers/acme/" + } + }, + "cloudflare-origin-ca": { + "name": "Cloudflare Origin CA documentation", + "basis": "checked October 2026", + "sources": { + "s1e15de5c9f36": "https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/" + } + }, + "openssl-cli": { + "name": "OpenSSL certificate commands", + "basis": "3.0", + "sources": { + "s00aaf106164f": "https://docs.openssl.org/3.0/man1/openssl-s_client/", + "s0856f4a58574": "https://docs.openssl.org/3.0/man1/openssl-x509/" + } + }, + "curl-source": { + "name": "curl command documentation", + "basis": "curl-8_12_1", + "sources": { + "s76f69702c9d5": "https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/head.md#L21-L22", + "s60820cd231e1": "https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/insecure.md#L21-L28" + } + } + }, + "claims": { + "public-trust": {"text": "ACME CAs such as Let's Encrypt and ZeroSSL issue free publicly trusted certificates; this guide advises preferring them for public DNS names, including private hosts using DNS-01.", "components": ["le", "zerossl"], "sources": ["le:s9ff2d787c618", "zerossl:sddca28a3381b"], "status": "REASONED"}, + "http-challenge": {"text": "HTTP-01 needs a public A/AAAA or CNAME pointing to the server and inbound internet access to port 80.", "components": ["le"], "sources": ["le:s9ff2d787c618"], "status": "REASONED"}, + "alpn-challenge": {"text": "TLS-ALPN-01 needs a public DNS record and inbound port 443; Caddy's ACME issuer enables the TLS-ALPN-01 solver when DNS-01 is not configured and TLS-ALPN-01 is not disabled, and can use an alternate local port if port 443 forwards to it; Traefik also uses TLS-ALPN-01.", "components": ["le", "caddy", "certmagic", "traefik"], "sources": ["le:s9ff2d787c618", "caddy:s409f9221aec2", "caddy:s80dbf0751153", "caddy:s5216789c9341", "certmagic:s61e3cba719df", "certmagic:s8782e006523b", "certmagic:s465c4ddcc92f", "certmagic:sc1e2ddefe5ed", "certmagic:sd423f53faa1a", "certmagic:sf2fbc79f6542", "certmagic:s1c3236df58b9", "traefik:s418a6b1ad09b"], "status": "REASONED"}, + "dns-challenge": {"text": "DNS-01 needs control of the challenge TXT record, no public address record or inbound port, and supports wildcards; automated issuance and renewal need an automated way to update TXT records, and this guide advises a provider DNS plugin and scoped API credentials, while Certbot also supports manual DNS-01 and an authentication hook for automatic renewal.", "components": ["le", "certbot", "certbot-v230"], "sources": ["le:s9ff2d787c618", "certbot:s7b60a00cadf3", "certbot:s875be163b4c1", "certbot-v230:s55ec19f37395", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"}, + "certbot-install": {"text": "Install Certbot and nginx/Apache plugins from the distribution, or the classic snap with /usr/bin/certbot pointing to /snap/bin/certbot.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "server-plugins": {"text": "certbot --nginx or --apache issues and installs certificates for the supplied domain names when the server is supported.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "standalone": {"text": "certonly --standalone binds port 80; stop its current owner and arrange webroot, a server plugin or port-freeing hooks for unattended renewal.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "webroot": {"text": "certonly --webroot uses /var/www/html to serve challenge files while the existing web server keeps running.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "dns-credentials": {"text": "The Cloudflare DNS plugin uses a credentials file and a Zone:DNS:Edit token limited to required zones; this guide requires never using the Global API Key and protecting the file with mode 600 in a 700 directory.", "components": ["certbot", "certbot-dns-cloudflare-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-dns-cloudflare-v230:s3f07ef3d544d", "certbot-dns-cloudflare-v230:sc3b33fa2c90f"], "status": "REASONED"}, + "certificate-paths": {"text": "Configure servers against stable /etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem paths for the chain and private key.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "renewal-schedule": {"text": "The Certbot v2.3.0 user guide says most installations have a scheduled task running certbot renew and says that, on Linux and BSD, you can check cron or systemd timers if unsure; this guide requires confirming the task and testing renewal plus reload with --dry-run --run-deploy-hooks, including package and snap installations.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-v230:s725109195132"], "status": "REASONED"}, + "deploy-hook": {"text": "This guide requires persisting a reload hook during successful initial issuance or successful renewal, or installing an executable in renewal-hooks/deploy; Certbot v2.3.0 saves the renewal-relevant options set on the command line to a lineage after successful renewal, and a renew --deploy-hook invocation with no renewal due does not save that hook.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-v230:s2f25bc233392", "certbot-v230:s11c7994242fc", "certbot-v230:sbb0c60d635ee", "certbot-v230:s7a053488f705", "certbot-v230:sa18c858c4e49", "certbot-v230:s2e4bd79a7795", "certbot-v230:s1910987bfd8c", "certbot-v230:s9c6118b72e0a", "certbot-v230:s3e22e32a2862"], "status": "REASONED"}, + "reconfigure": {"text": "certbot reconfigure can add the reload hook later on Certbot 2.3.0 and later.", "components": ["reconfigure"], "sources": ["reconfigure:s0dee7d6d65ba"], "status": "REASONED"}, + "expiry-monitoring": {"text": "Monitor the served certificate's expiry and renewal failures instead of relying on CA reminder emails; include a dry run in the deployment checklist. This guide assesses that any renewal step involving a person will fail at those scheduled shorter certificate lifetimes and that unattended automation is the only viable path.", "components": ["le", "certbot"], "sources": ["le:s4406ef210fff", "certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "default-lifetime": {"text": "At the guide's September 2026 documentation check, Let's Encrypt's default certificate lifetime is 90 days.", "components": ["le"], "sources": ["le:s01388c85c5d6"], "status": "REASONED"}, + "short-lifetime": {"text": "Optional 6-day short-lived certificates are available to every subscriber as of the guide's September 2026 check.", "components": ["le"], "sources": ["le:s01388c85c5d6"], "status": "REASONED"}, + "lifetime-schedule": {"text": "The classic profile moves to 64 days on 2027-02-10 and 45 days on 2028-02-16; industry rules cap publicly trusted certificates at 47 days from 2029-03-15.", "components": ["le"], "sources": ["le:s01388c85c5d6", "le:s4406ef210fff"], "status": "REASONED"}, + "revocation": {"text": "Let's Encrypt ended OCSP on 2025-08-06 and publishes revocation through CRLs only; do not add OCSP stapling directives for its certificates.", "components": ["le"], "sources": ["le:s21efa5995c48"], "status": "REASONED"}, + "staging": {"text": "Let's Encrypt limits per-domain issuance; test with certbot --staging or --test-cert before real issuance.", "components": ["le", "certbot"], "sources": ["le:s9ff2d787c618", "certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "caa": {"text": "CAA restricts issuing CAs, using issue letsencrypt.org for Let's Encrypt; certificate-transparency monitoring detects mis-issuance afterwards and complements CAA.", "components": ["le"], "sources": ["le:saf908bcd2143", "le:s9ff2d787c618"], "status": "REASONED"}, + "zerossl-eab": {"text": "ZeroSSL offers free ACME certificates; some clients need dashboard EAB credentials.", "components": ["zerossl"], "sources": ["zerossl:sddca28a3381b"], "status": "REASONED"}, + "acme-default": {"text": "acme.sh registers with ZeroSSL by default.", "components": ["acme"], "sources": ["acme:sa635c50fc1f9"], "status": "REASONED"}, + "acme-install": {"text": "acme.sh is a shell ACME client supporting many DNS providers; the guide recommends installation from a repository release instead of piping a download into a shell.", "components": ["acme"], "sources": ["acme:sa635c50fc1f9"], "status": "REASONED"}, + "server-automation": {"text": "This guide advises Caddy or Traefik for built-in issuance and renewal without an external client; it assesses them as the lowest-effort correct option for new deployments.", "components": ["le", "caddy", "traefik"], "sources": ["le:s9ff2d787c618", "caddy:s9683cb8d689f", "traefik:s418a6b1ad09b"], "status": "REASONED"}, + "origin-certificates": {"text": "Cloudflare lists Origin CA certificates on its Free plan, refers to long-lived origin CA certificates and says the validity period is chosen at creation, says they only encrypt traffic between Cloudflare and the origin, and warns that visitors may see untrusted certificate errors when Cloudflare is paused or proxying is disabled; this guide requires using them only for origins that receive traffic only through Cloudflare-proxied records.", "components": ["le", "cloudflare-origin-ca"], "sources": ["le:s9ff2d787c618", "cloudflare-origin-ca:s1e15de5c9f36"], "status": "REASONED"}, + "verify-inventory": {"text": "certbot certificates lists issued certificates and their expiry dates.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED", "verify": [1]}, + "verify-https": {"text": "Use the deployment's public hostname rather than example.com; curl HTTPS should succeed without -k.", "components": ["le", "curl-source"], "sources": ["le:s9ff2d787c618", "curl-source:s76f69702c9d5", "curl-source:s60820cd231e1"], "status": "REASONED", "verify": [1]}, + "verify-certificate": {"text": "The OpenSSL probe targets the hostname on port 443 with SNI, hostname verification and verification errors enabled, then displays issuer and dates.", "components": ["le", "openssl-cli"], "sources": ["le:s9ff2d787c618", "openssl-cli:s00aaf106164f", "openssl-cli:s0856f4a58574"], "status": "REASONED", "verify": [1]}, + "authentication": {"text": "A certificate alone is insufficient protection; continue with server controls and authentication.", "components": ["le"], "sources": ["le:s9ff2d787c618"], "status": "REASONED"}, + "dns-automation": {"text": "For automated DNS-01 issuance and renewal, this guide advises using a provider DNS plugin and least-privilege API credentials; Certbot also documents manual TXT entry and authentication hooks that automate the challenge.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s875be163b4c1", "certbot:s7b60a00cadf3", "certbot-v230:s55ec19f37395", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"}, + "dns-manual": {"text": "certbot certonly --manual --preferred-challenges dns permits hand-created TXT records; the Certbot v2.3.0 guide says certificates created with --manual do not support automatic renewal without a --manual-auth-hook that automates the challenge.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s7b60a00cadf3", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/gitops-controllers.md b/plugin/skills/secure-deployment-config/references/gitops-controllers.md index fdef731a6..0bdc1676b 100644 --- a/plugin/skills/secure-deployment-config/references/gitops-controllers.md +++ b/plugin/skills/secure-deployment-config/references/gitops-controllers.md @@ -1,318 +1,52 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-19", - "documentation_checked": "2026-09", - "body_sha256": "908b108054ff323b7d30f6ec6652728a0d065571804b124943b579b8f09f77cc", - "components": { - "argo": { - "name": "Argo CD v3.5.3 source", - "basis": "c9c369efcc5b2a0bd720803f8d14a1c3eaddf579", - "sources": { - "sddf4853ad503": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-service.yaml#L9-L20", - "s6e8c96d23e05": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-server/commands/argocd_server.go#L307-L323", - "s96065cc09776": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/server.go#L512-L535", - "s7cc690d1bf69": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/server.go#L668-L677", - "s028a4769d6c3": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/metrics/metrics.go#L74-L100", - "sc52bbd14bff4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/profile/profile.go#L11-L30", - "s98b27313f615": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-repo-server/commands/argocd_repo_server.go#L180-L212", - "sb0648e746fb9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L406-L408", - "s2f5af86c62d1": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L475-L481", - "s9bca471ac6d9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-repo-server/commands/argocd_repo_server.go#L257-L280", - "s4bbc848d9923": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/repo-server/argocd-repo-server-deployment.yaml#L367-L384", - "sf98b6bc4f5dc": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/application-controller/argocd-application-controller-statefulset.yaml#L360-L365", - "sf4dd846c82c4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/common/common.go#L75-L92", - "s49a7c83d9371": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-applicationset-controller/commands/applicationset_controller.go#L294-L296", - "s97a588d8a83f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/applicationset-controller/argocd-applicationset-controller-service.yaml#L9-L20", - "s0a85b38dfca8": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-notification/commands/argocd_notification.go#L149-L154", - "s5e141e32ed9c": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/notification/argocd-notifications-controller-metrics-service.yaml#L9-L16", - "s8efcc66f9cb4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-dex/commands/argocd_dex.go#L82-L119", - "s30c391d9dc23": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-dex/commands/argocd_dex.go#L143-L146", - "se477de2fdfa0": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/dex/config.go#L16-L152", - "s56c41c02ba8b": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-service.yaml#L9-L25", - "s996c72c5db28": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/redis/argocd-redis-deployment.yaml#L18-L58", - "s34909d7a1932": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/cache/cache.go#L233-L238", - "s1a13cc06db6d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/repo-server/argocd-repo-server-network-policy.yaml#L9-L35", - "s4bbf38e83770": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/redis/argocd-redis-network-policy.yaml#L9-L28", - "sadccbbcffcd8": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-network-policy.yaml#L9-L29", - "s743c8187cbd7": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-network-policy.yaml#L9-L16", - "s49f63c21c3cd": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/application-controller/argocd-application-controller-network-policy.yaml#L9-L19", - "sa258c4109d24": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/applicationset-controller/argocd-applicationset-controller-network-policy.yaml#L9-L22", - "sa49c30e332ed": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/notification/argocd-notifications-controller-network-policy.yaml#L9-L20", - "s68b67007da6b": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/settings/settings.go#L1631-L1638", - "s01a52daf078f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L121-L126", - "s210b24f04535": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/config/argocd-cmd-params-cm.yaml#L1-L7", - "s0f7f1b3404d9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/config/argocd-rbac-cm.yaml#L1-L7", - "s85445da68ce7": "https://github.com/argoproj/argo-cd/blob/v3.5.3/common/common.go#L89-L90", - "s357d253fc789": "https://github.com/argoproj/argo-cd/blob/v3.5.3/cmd/argocd-repo-server/commands/argocd_repo_server.go#L257-L260", - "s9df1243dde48": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-application-controller/commands/argocd_application_controller.go#L192-L209", - "s0abbc26d61e4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-application-controller/commands/argocd_application_controller.go#L272-L273", - "sfcaf2e03154d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/reposerver/server.go#L49-L85", - "s25a5da2c5e7c": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/reposerver/server.go#L94-L150", - "s73653423b85d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/tls/tls.go#L428-L506", - "s7c7bf274e3ef": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-deployment.yaml#L38-L42", - "s08939ffb906f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd/commands/admin/redis_initial_password.go#L22-L89", - "s3fcdf1601776": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L454-L467", - "se987196f20c7": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L539-L547", - "s3cb98aa5348d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L399-L425", - "s9bfcf07a6bcc": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L314-L346", - "s32323471d785": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L35-L42" - } - }, - "dex": { - "name": "Dex v2.45.1 source", - "basis": "v2.45.1", - "sources": { - "s3b9512b79dea": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/serve.go#L151-L199", - "s46eb2feaa84f": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/serve.go#L553-L572", - "s55b453f266f3": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/config.go#L255-L265" - } - }, - "redis-image": { - "name": "docker-library-redis v8.2.3 source", - "basis": "bcbc692165ce2241e194035041c5b5e01d2e1a5f", - "sources": { - "s67a2f7553c9a": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/Dockerfile#L81-L87", - "s30643c91686d": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/Dockerfile#L151-L154", - "sd04db8453288": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/docker-entrypoint.sh#L93-L111" - } - }, - "redis-source": { - "name": "Redis 8.2.3 source", - "basis": "8.2.3", - "sources": { - "s667fc16bf18f": "https://github.com/redis/redis/blob/8.2.3/src/server.h#L142-L143", - "saa497d27f72f": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2199-L2221", - "s0f96763525b7": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2638-L2673", - "s9b50a7c52721": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2983-L2996" - } - }, - "argo-docs": { - "name": "Argo CD documentation", - "basis": "unknown", - "sources": { - "s51e21b74ece6": "https://argo-cd.readthedocs.io/en/stable/operator-manual/security/", - "s02463a4fb268": "https://argo-cd.readthedocs.io/en/stable/operator-manual/secret-management/", - "s2efb9ee633af": "https://argo-cd.readthedocs.io/en/stable/getting_started/", - "sc2590df86f4e": "https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/", - "sf5f1c41f89bd": "https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/", - "s3767d400e364": "https://argo-cd.readthedocs.io/en/stable/operator-manual/ingress/", - "sdac90731b97d": "https://argo-cd.readthedocs.io/en/stable/operator-manual/webhook/", - "s9357e3b59e84": "https://argo-cd.readthedocs.io/en/stable/user-guide/projects/" - } - }, - "curl": { - "name": "curl", - "basis": "curl-8_14_1", - "sources": { - "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md", - "s9537bb790553": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data-binary.md#L4-L32", - "sd3b039bafbf6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data.md#L4-L49" - } - }, - "flux": { - "name": "Flux documentation", - "basis": "unknown", - "sources": { - "s493859f3b848": "https://fluxcd.io/flux/security/best-practices/", - "s4dcd744a2444": "https://fluxcd.io/flux/guides/mozilla-sops/", - "sd8618bbbc628": "https://fluxcd.io/flux/components/source/ocirepositories/#secret-reference", - "sf3f80d9941c8": "https://fluxcd.io/flux/installation/configuration/multitenancy/", - "s0669b973542c": "https://fluxcd.io/flux/security/#controller-permissions", - "s13bd7f0204b0": "https://fluxcd.io/flux/installation/bootstrap/github/", - "s8759854c1bdf": "https://fluxcd.io/flux/components/image/imageupdateautomations/", - "s0acf42beed45": "https://fluxcd.io/flux/guides/webhook-receivers/", - "s35bb9a844dcf": "https://fluxcd.io/flux/components/notification/receivers/" - } - }, - "flux-oidc": { - "name": "Flux OIDC introduction", - "basis": "v2.9", - "sources": { - "s1ea193b2b3cc": "https://fluxcd.io/blog/2026/06/flux-v2.9.0/" - } - }, - "python": { - "name": "Python documentation", - "basis": "unknown", - "sources": { - "s7a54556e5dac": "https://docs.python.org/3/library/hmac.html", - "s31fccd7d50e5": "https://docs.python.org/3/library/sys.html#sys.stdin" - } - } - }, - "claims": { - "git-boundary": {"text": "Repository writers exercise reconciliation permissions; review platform changes, separate tenants and scope credentials.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s51e21b74ece6", "flux:s493859f3b848"], "status": "REASONED"}, - "secret-storage": {"text": "Protect bootstrap, registry, cluster, webhook and decryption credentials; commit only encrypted secrets and keep SOPS/age keys out of Git.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s02463a4fb268", "flux:s4dcd744a2444", "flux:sd8618bbbc628"], "status": "REASONED"}, - "egress": {"text": "Restrict fetch, API, identity and notification egress; Argo repository allowlists do not constrain remote bases or Helm dependencies.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s51e21b74ece6", "flux:sf3f80d9941c8"], "status": "REASONED"}, - "argo-bootstrap": {"text": "Rotate the initial admin password before deleting argocd-initial-admin-secret; deletion alone does not rotate it.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:sc2590df86f4e"], "status": "REASONED"}, - "argo-sso": {"text": "The built-in admin is unrestricted; configure named SSO roles and IdP MFA, then set admin.enabled=false.", "components": ["argo-docs"], "sources": ["argo-docs:sc2590df86f4e", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, - "argo-origin": {"text": "Keep the ClusterIP origin private behind authenticated TLS and protect both REST/browser and CLI/gRPC routes.", "components": ["argo-docs", "argo"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:s3767d400e364", "argo:sddf4853ad503"], "status": "REASONED"}, - "argo-server": {"text": "Server defaults to 0.0.0.0:8080; the Service maps 80 and 443 to it.", "components": ["argo"], "sources": ["argo:s6e8c96d23e05", "argo:sddf4853ad503", "argo:sf4dd846c82c4"], "status": "REASONED"}, - "argo-tls": {"text": "Server --insecure/server.insecure defaults false and disables backend TLS, not authentication; use only behind protected TLS termination.", "components": ["argo", "argo-docs"], "sources": ["argo:s6e8c96d23e05", "argo-docs:s3767d400e364"], "status": "REASONED"}, - "argo-client-tls": {"text": "argocd login --insecure skips client certificate verification; the server initially uses a self-signed certificate.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:s3767d400e364"], "status": "REASONED"}, - "argo-metrics": {"text": "Server HTTP metrics on 8083 are unauthenticated and bind to --address, not --metrics-address.", "components": ["argo"], "sources": ["argo:s96065cc09776", "argo:s7cc690d1bf69", "argo:s028a4769d6c3", "argo:sf4dd846c82c4", "argo:s6e8c96d23e05"], "status": "REASONED"}, - "argo-profiler": {"text": "Server and repo-server pprof paths return 401 unless the profiler file contains exactly true with no newline; enabled profiling has no caller authentication.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:s028a4769d6c3", "argo:s98b27313f615"], "status": "REASONED"}, - "argo-profiler-file": {"text": "ARGOCD_ENABLE_PROFILER_FILE_PATH defaults to /home/argocd/params/profiler.enabled; the server mounts ConfigMap key server.profile.enabled there.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:sb0648e746fb9", "argo:s2f5af86c62d1"], "status": "REASONED"}, - "repo-grpc": {"text": "Repo-server gRPC binds 0.0.0.0:8081 with TLS by default; mTLS is skipped without the client CA, separate from the serving certificate.", "components": ["argo"], "sources": ["argo:s9bca471ac6d9", "argo:s4bbc848d9923", "argo:sf4dd846c82c4", "argo:s85445da68ce7", "argo:s357d253fc789"], "status": "REASONED"}, - "repo-http": {"text": "Repo-server HTTP metrics and health bind 0.0.0.0:8084 without an authentication wrapper.", "components": ["argo"], "sources": ["argo:s98b27313f615", "argo:s9bca471ac6d9", "argo:sf4dd846c82c4", "argo:s85445da68ce7", "argo:s357d253fc789"], "status": "REASONED"}, - "repo-limits": {"text": "Repo-server requires verified client certificates with a loaded client CA; missing or unstatable serving files trigger a session self-signed certificate, while an invalid present pair errors. No authorization interceptor appears; service method authorization is unverified.", "components": ["argo"], "sources": ["argo:sfcaf2e03154d", "argo:s25a5da2c5e7c", "argo:s73653423b85d"], "status": "REASONED"}, - "controller-metrics": {"text": "Application-controller metrics and health use 8082; the command passes that port to the controller, but the fetched files do not establish the bind address.", "components": ["argo"], "sources": ["argo:sf98b6bc4f5dc", "argo:s9df1243dde48", "argo:s0abbc26d61e4", "argo:sf4dd846c82c4"], "status": "REASONED"}, - "applicationset-webhook": {"text": "ApplicationSet webhook binds :7000; its container and Service declare 7000.", "components": ["argo"], "sources": ["argo:s49a7c83d9371", "argo:s97a588d8a83f"], "status": "REASONED"}, - "applicationset-metrics": {"text": "ApplicationSet metrics bind :8080; its container and Service declare 8080.", "components": ["argo"], "sources": ["argo:s49a7c83d9371", "argo:s97a588d8a83f"], "status": "REASONED"}, - "applicationset-health": {"text": "ApplicationSet health probes bind :8081 without a container or Service port declaration; missing declarations do not close sockets.", "components": ["argo"], "sources": ["argo:s49a7c83d9371"], "status": "REASONED"}, - "notifications": {"text": "Notifications HTTP metrics bind 0.0.0.0:9001 without authentication; a metrics Service and TCP liveness probe exist without containerPort.", "components": ["argo"], "sources": ["argo:s0a85b38dfca8", "argo:s5e141e32ed9c"], "status": "REASONED"}, - "dex-http": {"text": "Dex starts only with nonempty generated configuration; HTTP defaults to TLS on 0.0.0.0:5556 using /tmp/tls.crt and /tmp/tls.key.", "components": ["argo"], "sources": ["argo:s8efcc66f9cb4", "argo:s30c391d9dc23", "argo:se477de2fdfa0"], "status": "REASONED"}, - "dex-grpc": {"text": "Generated Dex gRPC uses 0.0.0.0:5557 without TLS or client-auth settings; Dex v2.45.1 source sets no transport credentials or authentication interceptor for this configuration, while API method authorization is unverified; this assumes the deployed ghcr.io/dexidp/dex:v2.45.1 tag was built from Dex v2.45.1.", "components": ["argo", "dex"], "sources": ["argo:se477de2fdfa0", "argo:s56c41c02ba8b", "argo:s7c7bf274e3ef", "dex:s3b9512b79dea", "dex:s46eb2feaa84f", "dex:s55b453f266f3"], "status": "REASONED"}, - "dex-telemetry": {"text": "Dex telemetry uses HTTP on 0.0.0.0:5558; all three Dex ports have Services.", "components": ["argo"], "sources": ["argo:se477de2fdfa0", "argo:s56c41c02ba8b"], "status": "REASONED"}, - "redis-password": {"text": "Redis receives --requirepass from argocd-redis/auth; secret-init generates and checks that Secret. The redis:8.2.3-alpine tag, taken as built from docker-library-redis v8.2.3, uses Redis wildcard bind defaults and patches protected mode off.", "components": ["argo", "redis-image", "redis-source"], "sources": ["argo:s996c72c5db28", "argo:s08939ffb906f", "redis-image:s67a2f7553c9a", "redis-image:s30643c91686d", "redis-image:sd04db8453288", "redis-source:s667fc16bf18f", "redis-source:saa497d27f72f", "redis-source:s0f96763525b7", "redis-source:s9b50a7c52721"], "status": "REASONED"}, - "redis-tls": {"text": "Base manifests configure no Redis TLS and the Argo Redis client defaults --redis-use-tls=false; protect traffic, Secret and cache.", "components": ["argo"], "sources": ["argo:s996c72c5db28", "argo:s34909d7a1932"], "status": "REASONED"}, - "component-policies": {"text": "Base policies restrict repo gRPC, Redis and Dex callers by component; enforcement needs a supporting CNI.", "components": ["argo"], "sources": ["argo:s1a13cc06db6d", "argo:s4bbf38e83770", "argo:sadccbbcffcd8"], "status": "REASONED"}, - "broad-policies": {"text": "Server policy allows all ingress; other metrics and ApplicationSet webhook allowances admit every namespace. Narrow existing additive allowances and egress separately.", "components": ["argo"], "sources": ["argo:s743c8187cbd7", "argo:s49f63c21c3cd", "argo:sa258c4109d24", "argo:sa49c30e332ed"], "status": "REASONED"}, - "argo-anonymous": {"text": "users.anonymous.enabled defaults off; anonymous callers otherwise inherit policy.default.", "components": ["argo", "argo-docs"], "sources": ["argo:s68b67007da6b", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, - "argo-disable-auth": {"text": "server.disable.auth defaults false; inspect effective args and environment because anonymous-off does not compensate for disabled authentication.", "components": ["argo"], "sources": ["argo:s6e8c96d23e05", "argo:s01a52daf078f", "argo:s210b24f04535"], "status": "REASONED"}, - "argo-default-role": {"text": "Base RBAC ConfigMap omits policy.default; the inspected loader supplies an empty role to the enforcer, which skips its additive default-role check. Default-role grants are additive and cannot be revoked by a later subject rule; define scoped policy.csv roles and group mappings. Server-side wiring remains unverified.", "components": ["argo", "argo-docs"], "sources": ["argo:s0f7f1b3404d9", "argo:s3fcdf1601776", "argo:se987196f20c7", "argo:s3cb98aa5348d", "argo:s9bfcf07a6bcc", "argo:s32323471d785", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, - "argo-webhook": {"text": "/api/webhook accepts unauthenticated refresh events without a shared secret; configure one to limit spoofed reconciliation and resource exhaustion.", "components": ["argo-docs"], "sources": ["argo-docs:sdac90731b97d"], "status": "REASONED"}, - "argo-projects": {"text": "Restrict AppProjects including the built-in default project; project constraints do not reduce a compromised controller's Kubernetes credentials.", "components": ["argo-docs"], "sources": ["argo-docs:s9357e3b59e84", "argo-docs:s51e21b74ece6"], "status": "REASONED"}, - "argo-cluster-rbac": {"text": "Reduce controller and registered-cluster argocd-manager write permissions to needed namespaces/resources while retaining required read access.", "components": ["argo-docs"], "sources": ["argo-docs:s51e21b74ece6"], "status": "REASONED"}, - "flux-api": {"text": "Flux has no standalone management login/API; Kubernetes authenticates its custom-resource operations, while separate dashboards need their own controls.", "components": ["flux"], "sources": ["flux:s493859f3b848", "flux:s0669b973542c"], "status": "REASONED"}, - "flux-bootstrap": {"text": "GitHub bootstrap --token-auth retains a PAT in flux-system; --token-auth=false creates a read-only SSH deploy key by default. Protect bootstrap input and Secrets.", "components": ["flux"], "sources": ["flux:s13bd7f0204b0"], "status": "REASONED"}, - "flux-writeback": {"text": "Image automation needs Git write access, including --read-write-key=true for the bootstrap key; scope the repository and protect update branches and objects.", "components": ["flux"], "sources": ["flux:s13bd7f0204b0", "flux:s8759854c1bdf"], "status": "REASONED"}, - "flux-artifacts": {"text": "Source-controller serves fetched artifacts over internal HTTP; enforced NetworkPolicy, not ClusterIP alone, isolates them.", "components": ["flux"], "sources": ["flux:s493859f3b848"], "status": "REASONED"}, - "flux-receiver-port": {"text": "Webhook receiver listens on 9292 behind Service port 80; external exposure needs a route and TLS termination.", "components": ["flux"], "sources": ["flux:s0acf42beed45"], "status": "REASONED"}, - "flux-receiver-types": {"text": "generic validates nothing; generic-hmac, GitHub, Bitbucket and Nexus use HMAC, while gitlab compares X-Gitlab-Token.", "components": ["flux"], "sources": ["flux:s35bb9a844dcf"], "status": "REASONED"}, - "flux-oidc": {"text": "generic-oidc, introduced in Flux 2.9, rejects secretRef and validates bearer tokens; constrain issuer validations and audience, whose default is notification-controller.", "components": ["flux", "flux-oidc"], "sources": ["flux:s35bb9a844dcf", "flux-oidc:s1ea193b2b3cc"], "status": "REASONED"}, - "flux-rbac": {"text": "Only kustomize-controller and helm-controller hold cluster-admin; other controllers still have meaningful permissions including Secret access.", "components": ["flux"], "sources": ["flux:s0669b973542c"], "status": "REASONED"}, - "flux-lockdown": {"text": "Use scoped reconciliation ServiceAccounts, --no-cross-namespace-refs and --no-remote-bases; --default-service-account fills an omission, not an explicit serviceAccountName.", "components": ["flux"], "sources": ["flux:sf3f80d9941c8"], "status": "REASONED"}, - "verify-argo": {"text": "Anonymous application listing should fail with 401 while a valid token lists the expected application; repeat at origin and CLI/gRPC and inspect effective RBAC.", "components": ["argo-docs", "argo", "curl"], "sources": ["argo-docs:sf5f1c41f89bd", "argo:s6e8c96d23e05", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, - "verify-bootstrap": {"text": "Absence of the bootstrap Secret proves removal of that material, not password rotation.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af"], "status": "REASONED", "verify": [1]}, - "verify-inventory": {"text": "Inventory Services, routes, host ports, policies and component sockets; node-local ss and policy objects alone do not establish cluster isolation.", "components": ["argo", "flux"], "sources": ["argo:s743c8187cbd7", "flux:s493859f3b848"], "status": "REASONED", "verify": [1]}, - "verify-repo-metrics": {"text": "Base policy permits HTTP 200 metrics to monitoring and unrelated pods; narrowed policy should admit only monitoring, with a successful positive control.", "components": ["argo"], "sources": ["argo:s98b27313f615", "argo:s1a13cc06db6d"], "status": "REASONED", "verify": [2]}, - "verify-server-metrics": {"text": "Repeat on server 8083: only authorized monitoring should receive metrics after narrowing ingress; a failed control is inconclusive.", "components": ["argo"], "sources": ["argo:s028a4769d6c3", "argo:s743c8187cbd7"], "status": "REASONED"}, - "verify-profiler": {"text": "From an allowed pod, pprof cmdline should return 401 disabled, 200 for exact true, then 401 for true plus newline; keep metrics as control.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:s028a4769d6c3"], "status": "REASONED"}, - "verify-flux": {"text": "Identical generic-hmac bodies without and with X-Signature should be rejected and accepted with reconciliation; status is type/version dependent. The cited curl curl-8_14_1 lines show --data-binary posting data exactly as specified, preserving newlines and carriage returns that --data strips from @file input, reading @- from stdin and @filename from a file, and posting like --data's POST request.", "components": ["flux", "python", "curl"], "sources": ["flux:s35bb9a844dcf", "python:s7a54556e5dac", "python:s31fccd7d50e5", "curl:s3db522258e44", "curl:s9537bb790553", "curl:sd3b039bafbf6"], "status": "REASONED", "verify": [3]}, - "verify-receiver-status": {"text": "Read receiver name and Ready condition without exposing webhookPath or condition messages; origin checks distinguish receiver rejection from proxy rejection.", "components": ["flux"], "sources": ["flux:s35bb9a844dcf"], "status": "REASONED"} - } -} ---- # GitOps controllers: Argo CD and Flux - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +Written against Argo CD v3.5.3 source (pinned to a commit), Flux OIDC introduction v2.9, Dex v2.45.1 source v2.45.1, Redis 8.2.3 source 8.2.3, and curl curl-8_14_1; 1 further source is pinned to a commit and 3 carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 50 claims, all reasoned from cited sources. -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. -Components (recorded basis): +Argo CD and Flux reconcile a git repository into a Kubernetes cluster, so both hold +cluster-admin-equivalent power: whoever can drive the controller, or change what it reconciles, can +deploy anything it can reach, across the clusters it is registered against and to the extent the controller's Kubernetes RBAC, its registered-cluster credentials, and project restrictions allow. Three exposures follow +from that and apply to both. The blast radius is the cluster (or clusters), so administrative access +deserves the same care as the Kubernetes API itself ([kubernetes.md](kubernetes.md)). Git is an +authorization boundary: whoever can merge to a reconciled repository, or redirect a source, exercises +the controller's permissions, so platform repositories need reviewed changes and separation from +tenant repositories. And the credentials the controllers hold (git deploy keys, cluster Secrets) +should be read-only and narrowly scoped where the work allows. Handle every credential class per +[secrets.md](secrets.md): bootstrap tokens, git and registry credentials (Flux `OCIRepository`/`HelmRepository` +`.spec.secretRef`), cluster credentials, webhook secrets, and decryption keys. Never commit plaintext +credentials or base64-only Secret manifests; encrypted SOPS files and SealedSecret resources may be +committed, but Sealed Secrets is a separate controller and SOPS/age decryption keys (Flux references them +from the Kustomization) must be protected and never committed. For Argo CD, prefer destination-cluster +secret operators, because config-management plugins can surface decrypted values through Redis and the +repo-server. The tools differ in one way that shapes +the rest: Argo CD runs a network-facing API and UI with its own accounts, while Flux has no standalone +management UI or user-login API of its own, and is driven through Kubernetes custom resources under the +API server's authentication and RBAC (its controllers still expose in-cluster HTTP endpoints, and a +separately installed Flux dashboard brings its own login and exposure to harden). Control-plane exposure itself, the API server and etcd, stays in +[kubernetes.md](kubernetes.md). Egress is a surface too: both controllers fetch from Git, container and +chart registries, the Kubernetes API, identity/KMS, and notification targets, and Argo CD's repository +allowlist does not constrain remote bases or Helm chart dependencies while its webhook path has an SSRF +consideration, so restrict controller egress to the destinations each deployment needs and apply the +metadata protections in [egress-metadata.md](egress-metadata.md), keeping only the metadata access the +identity mechanism requires (for Flux tenant reconciliation, `--no-remote-bases=true` where applicable). -- argo: Argo CD v3.5.3 source (c9c369efcc5b2a0bd720803f8d14a1c3eaddf579) -- dex: Dex v2.45.1 source (v2.45.1) -- redis-image: docker-library-redis v8.2.3 source (bcbc692165ce2241e194035041c5b5e01d2e1a5f) -- redis-source: Redis 8.2.3 source (8.2.3) -- argo-docs: Argo CD documentation (unknown) -- curl: curl (curl-8_14_1) -- flux: Flux documentation (unknown) -- flux-oidc: Flux OIDC introduction (v2.9) -- python: Python documentation (unknown) +## Argo CD -Claims (full source mapping in the front matter): - -- git-boundary (REASONED; argo-docs, flux): Repository writers exercise reconciliation permissions; review platform changes, separate tenants and scope credentials. -- secret-storage (REASONED; argo-docs, flux): Protect bootstrap, registry, cluster, webhook and decryption credentials; commit only encrypted secrets and keep SOPS/age keys out of Git. -- egress (REASONED; argo-docs, flux): Restrict fetch, API, identity and notification egress; Argo repository allowlists do not constrain remote bases or Helm dependencies. -- argo-bootstrap (REASONED; argo-docs): Rotate the initial admin password before deleting argocd-initial-admin-secret; deletion alone does not rotate it. -- argo-sso (REASONED; argo-docs): The built-in admin is unrestricted; configure named SSO roles and IdP MFA, then set admin.enabled=false. -- argo-origin (REASONED; argo-docs, argo): Keep the ClusterIP origin private behind authenticated TLS and protect both REST/browser and CLI/gRPC routes. -- argo-server (REASONED; argo): Server defaults to 0.0.0.0:8080; the Service maps 80 and 443 to it. -- argo-tls (REASONED; argo, argo-docs): Server --insecure/server.insecure defaults false and disables backend TLS, not authentication; use only behind protected TLS termination. -- argo-client-tls (REASONED; argo-docs): argocd login --insecure skips client certificate verification; the server initially uses a self-signed certificate. -- argo-metrics (REASONED; argo): Server HTTP metrics on 8083 are unauthenticated and bind to --address, not --metrics-address. -- argo-profiler (REASONED; argo): Server and repo-server pprof paths return 401 unless the profiler file contains exactly true with no newline; enabled profiling has no caller authentication. -- argo-profiler-file (REASONED; argo): ARGOCD_ENABLE_PROFILER_FILE_PATH defaults to /home/argocd/params/profiler.enabled; the server mounts ConfigMap key server.profile.enabled there. -- repo-grpc (REASONED; argo): Repo-server gRPC binds 0.0.0.0:8081 with TLS by default; mTLS is skipped without the client CA, separate from the serving certificate. -- repo-http (REASONED; argo): Repo-server HTTP metrics and health bind 0.0.0.0:8084 without an authentication wrapper. -- repo-limits (REASONED; argo): Repo-server requires verified client certificates with a loaded client CA; missing or unstatable serving files trigger a session self-signed certificate, while an invalid present pair errors. No authorization interceptor appears; service method authorization is unverified. -- controller-metrics (REASONED; argo): Application-controller metrics and health use 8082; the command passes that port to the controller, but the fetched files do not establish the bind address. -- applicationset-webhook (REASONED; argo): ApplicationSet webhook binds :7000; its container and Service declare 7000. -- applicationset-metrics (REASONED; argo): ApplicationSet metrics bind :8080; its container and Service declare 8080. -- applicationset-health (REASONED; argo): ApplicationSet health probes bind :8081 without a container or Service port declaration; missing declarations do not close sockets. -- notifications (REASONED; argo): Notifications HTTP metrics bind 0.0.0.0:9001 without authentication; a metrics Service and TCP liveness probe exist without containerPort. -- dex-http (REASONED; argo): Dex starts only with nonempty generated configuration; HTTP defaults to TLS on 0.0.0.0:5556 using /tmp/tls.crt and /tmp/tls.key. -- dex-grpc (REASONED; argo, dex): Generated Dex gRPC uses 0.0.0.0:5557 without TLS or client-auth settings; Dex v2.45.1 source sets no transport credentials or authentication interceptor for this configuration, while API method authorization is unverified; this assumes the deployed ghcr.io/dexidp/dex:v2.45.1 tag was built from Dex v2.45.1. -- dex-telemetry (REASONED; argo): Dex telemetry uses HTTP on 0.0.0.0:5558; all three Dex ports have Services. -- redis-password (REASONED; argo, redis-image, redis-source): Redis receives --requirepass from argocd-redis/auth; secret-init generates and checks that Secret. The redis:8.2.3-alpine tag, taken as built from docker-library-redis v8.2.3, uses Redis wildcard bind defaults and patches protected mode off. -- redis-tls (REASONED; argo): Base manifests configure no Redis TLS and the Argo Redis client defaults --redis-use-tls=false; protect traffic, Secret and cache. -- component-policies (REASONED; argo): Base policies restrict repo gRPC, Redis and Dex callers by component; enforcement needs a supporting CNI. -- broad-policies (REASONED; argo): Server policy allows all ingress; other metrics and ApplicationSet webhook allowances admit every namespace. Narrow existing additive allowances and egress separately. -- argo-anonymous (REASONED; argo, argo-docs): users.anonymous.enabled defaults off; anonymous callers otherwise inherit policy.default. -- argo-disable-auth (REASONED; argo): server.disable.auth defaults false; inspect effective args and environment because anonymous-off does not compensate for disabled authentication. -- argo-default-role (REASONED; argo, argo-docs): Base RBAC ConfigMap omits policy.default; the inspected loader supplies an empty role to the enforcer, which skips its additive default-role check. Default-role grants are additive and cannot be revoked by a later subject rule; define scoped policy.csv roles and group mappings. Server-side wiring remains unverified. -- argo-webhook (REASONED; argo-docs): /api/webhook accepts unauthenticated refresh events without a shared secret; configure one to limit spoofed reconciliation and resource exhaustion. -- argo-projects (REASONED; argo-docs): Restrict AppProjects including the built-in default project; project constraints do not reduce a compromised controller's Kubernetes credentials. -- argo-cluster-rbac (REASONED; argo-docs): Reduce controller and registered-cluster argocd-manager write permissions to needed namespaces/resources while retaining required read access. -- flux-api (REASONED; flux): Flux has no standalone management login/API; Kubernetes authenticates its custom-resource operations, while separate dashboards need their own controls. -- flux-bootstrap (REASONED; flux): GitHub bootstrap --token-auth retains a PAT in flux-system; --token-auth=false creates a read-only SSH deploy key by default. Protect bootstrap input and Secrets. -- flux-writeback (REASONED; flux): Image automation needs Git write access, including --read-write-key=true for the bootstrap key; scope the repository and protect update branches and objects. -- flux-artifacts (REASONED; flux): Source-controller serves fetched artifacts over internal HTTP; enforced NetworkPolicy, not ClusterIP alone, isolates them. -- flux-receiver-port (REASONED; flux): Webhook receiver listens on 9292 behind Service port 80; external exposure needs a route and TLS termination. -- flux-receiver-types (REASONED; flux): generic validates nothing; generic-hmac, GitHub, Bitbucket and Nexus use HMAC, while gitlab compares X-Gitlab-Token. -- flux-oidc (REASONED; flux, flux-oidc): generic-oidc, introduced in Flux 2.9, rejects secretRef and validates bearer tokens; constrain issuer validations and audience, whose default is notification-controller. -- flux-rbac (REASONED; flux): Only kustomize-controller and helm-controller hold cluster-admin; other controllers still have meaningful permissions including Secret access. -- flux-lockdown (REASONED; flux): Use scoped reconciliation ServiceAccounts, --no-cross-namespace-refs and --no-remote-bases; --default-service-account fills an omission, not an explicit serviceAccountName. -- verify-argo (REASONED; argo-docs, argo, curl): Anonymous application listing should fail with 401 while a valid token lists the expected application; repeat at origin and CLI/gRPC and inspect effective RBAC. -- verify-bootstrap (REASONED; argo-docs): Absence of the bootstrap Secret proves removal of that material, not password rotation. -- verify-inventory (REASONED; argo, flux): Inventory Services, routes, host ports, policies and component sockets; node-local ss and policy objects alone do not establish cluster isolation. -- verify-repo-metrics (REASONED; argo): Base policy permits HTTP 200 metrics to monitoring and unrelated pods; narrowed policy should admit only monitoring, with a successful positive control. -- verify-server-metrics (REASONED; argo): Repeat on server 8083: only authorized monitoring should receive metrics after narrowing ingress; a failed control is inconclusive. -- verify-profiler (REASONED; argo): From an allowed pod, pprof cmdline should return 401 disabled, 200 for exact true, then 401 for true plus newline; keep metrics as control. -- verify-flux (REASONED; flux, python, curl): Identical generic-hmac bodies without and with X-Signature should be rejected and accepted with reconciliation; status is type/version dependent. The cited curl curl-8_14_1 lines show --data-binary posting data exactly as specified, preserving newlines and carriage returns that --data strips from @file input, reading @- from stdin and @filename from a file, and posting like --data's POST request. -- verify-receiver-status (REASONED; flux): Read receiver name and Ready condition without exposing webhookPath or condition messages; origin checks distinguish receiver rejection from proxy rejection. - - -Argo CD and Flux reconcile a git repository into a Kubernetes cluster, so both hold -cluster-admin-equivalent power: whoever can drive the controller, or change what it reconciles, can -deploy anything it can reach, across the clusters it is registered against and to the extent the controller's Kubernetes RBAC, its registered-cluster credentials, and project restrictions allow. Three exposures follow -from that and apply to both. The blast radius is the cluster (or clusters), so administrative access -deserves the same care as the Kubernetes API itself ([kubernetes.md](kubernetes.md)). Git is an -authorization boundary: whoever can merge to a reconciled repository, or redirect a source, exercises -the controller's permissions, so platform repositories need reviewed changes and separation from -tenant repositories. And the credentials the controllers hold (git deploy keys, cluster Secrets) -should be read-only and narrowly scoped where the work allows. Handle every credential class per -[secrets.md](secrets.md): bootstrap tokens, git and registry credentials (Flux `OCIRepository`/`HelmRepository` -`.spec.secretRef`), cluster credentials, webhook secrets, and decryption keys. Never commit plaintext -credentials or base64-only Secret manifests; encrypted SOPS files and SealedSecret resources may be -committed, but Sealed Secrets is a separate controller and SOPS/age decryption keys (Flux references them -from the Kustomization) must be protected and never committed. For Argo CD, prefer destination-cluster -secret operators, because config-management plugins can surface decrypted values through Redis and the -repo-server. The tools differ in one way that shapes -the rest: Argo CD runs a network-facing API and UI with its own accounts, while Flux has no standalone -management UI or user-login API of its own, and is driven through Kubernetes custom resources under the -API server's authentication and RBAC (its controllers still expose in-cluster HTTP endpoints, and a -separately installed Flux dashboard brings its own login and exposure to harden). Control-plane exposure itself, the API server and etcd, stays in -[kubernetes.md](kubernetes.md). Egress is a surface too: both controllers fetch from Git, container and -chart registries, the Kubernetes API, identity/KMS, and notification targets, and Argo CD's repository -allowlist does not constrain remote bases or Helm chart dependencies while its webhook path has an SSRF -consideration, so restrict controller egress to the destinations each deployment needs and apply the -metadata protections in [egress-metadata.md](egress-metadata.md), keeping only the metadata access the -identity mechanism requires (for Flux tenant reconciliation, `--no-remote-bases=true` where applicable). - -## Argo CD - -The install generates an initial `admin` password and stores it in cleartext in a Secret named -`argocd-initial-admin-secret`, in the `password` field. Rotate it and then delete the Secret: -`argocd account update-password` changes the password, and the vendor says the Secret "can safely be -deleted" afterward since it only holds the bootstrap value. Deleting the Secret does not by itself -rotate the credential, so change the password first. Give `argocd account update-password` the current -and new passwords at the tool's prompt, not as arguments: `--current-password` and `--new-password` -would put them in its argv. Sources has no reference for the command prompting (its Argo CD user -management entry is described as covering disabling admin and changing the password), so confirm -that it does before relying on it. A prompt keeps the passwords out of the command text, shell -history and shell tracing; the account owner and root can still read them in the `argocd` process -while it runs. `admin` is the only built-in user and a -superuser with unrestricted access; once you have configured SSO (OIDC or Dex) and granted named -roles, disable it with `admin.enabled: "false"` in the `argocd-cm` ConfigMap, and pair Argo CD logins -with your identity provider's MFA ([mfa.md](mfa.md)). +The install generates an initial `admin` password and stores it in cleartext in a Secret named +`argocd-initial-admin-secret`, in the `password` field. Rotate it and then delete the Secret: +`argocd account update-password` changes the password, and the vendor says the Secret "can safely be +deleted" afterward since it only holds the bootstrap value. Deleting the Secret does not by itself +rotate the credential, so change the password first. Give `argocd account update-password` the current +and new passwords at the tool's prompt, not as arguments: `--current-password` and `--new-password` +would put them in its argv. Sources has no reference for the command prompting (its Argo CD user +management entry is described as covering disabling admin and changing the password), so confirm +that it does before relying on it. A prompt keeps the passwords out of the command text, shell +history and shell tracing; the account owner and root can still read them in the `argocd` process +while it runs. `admin` is the only built-in user and a +superuser with unrestricted access; once you have configured SSO (OIDC or Dex) and granted named +roles, disable it with `admin.enabled: "false"` in the `argocd-cm` ConfigMap, and pair Argo CD logins +with your identity provider's MFA ([mfa.md](mfa.md)). The quickstart does not expose the API and UI outside the cluster; `argocd-server` is a ClusterIP Service, and reaching it is a deliberate act through a LoadBalancer, an Ingress, a Gateway route, or @@ -729,3 +463,295 @@ they were not rechecked online. Remaining source limits are identified above. - Argo CD Redis password initializer command (v3.5.3; commit c9c369efcc5b2a0bd720803f8d14a1c3eaddf579; checked October 2026): https://github.com/argoproj/argo-cd/blob/v3.5.3/cmd/argocd/commands/admin/redis_initial_password.go#L41-L42 - Argo CD additive default-role enforcement (v3.5.3; commit c9c369efcc5b2a0bd720803f8d14a1c3eaddf579; checked October 2026): https://github.com/argoproj/argo-cd/blob/v3.5.3/util/rbac/rbac.go#L401-L406 - Argo CD default-role ConfigMap loading (v3.5.3; commit c9c369efcc5b2a0bd720803f8d14a1c3eaddf579; checked October 2026): https://github.com/argoproj/argo-cd/blob/v3.5.3/util/rbac/rbac.go#L540-L541 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/gitops-controllers.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- argo: Argo CD v3.5.3 source (c9c369efcc5b2a0bd720803f8d14a1c3eaddf579) +- flux-oidc: Flux OIDC introduction (v2.9) +- argo-docs: Argo CD documentation (unknown) +- flux: Flux documentation (unknown) +- dex: Dex v2.45.1 source (v2.45.1) +- redis-image: docker-library-redis v8.2.3 source (bcbc692165ce2241e194035041c5b5e01d2e1a5f) +- redis-source: Redis 8.2.3 source (8.2.3) +- curl: curl (curl-8_14_1) +- python: Python documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- git-boundary (REASONED; argo-docs, flux): Repository writers exercise reconciliation permissions; review platform changes, separate tenants and scope credentials. +- secret-storage (REASONED; argo-docs, flux): Protect bootstrap, registry, cluster, webhook and decryption credentials; commit only encrypted secrets and keep SOPS/age keys out of Git. +- egress (REASONED; argo-docs, flux): Restrict fetch, API, identity and notification egress; Argo repository allowlists do not constrain remote bases or Helm dependencies. +- argo-bootstrap (REASONED; argo-docs): Rotate the initial admin password before deleting argocd-initial-admin-secret; deletion alone does not rotate it. +- argo-sso (REASONED; argo-docs): The built-in admin is unrestricted; configure named SSO roles and IdP MFA, then set admin.enabled=false. +- argo-origin (REASONED; argo-docs, argo): Keep the ClusterIP origin private behind authenticated TLS and protect both REST/browser and CLI/gRPC routes. +- argo-server (REASONED; argo): Server defaults to 0.0.0.0:8080; the Service maps 80 and 443 to it. +- argo-tls (REASONED; argo, argo-docs): Server --insecure/server.insecure defaults false and disables backend TLS, not authentication; use only behind protected TLS termination. +- argo-client-tls (REASONED; argo-docs): argocd login --insecure skips client certificate verification; the server initially uses a self-signed certificate. +- argo-metrics (REASONED; argo): Server HTTP metrics on 8083 are unauthenticated and bind to --address, not --metrics-address. +- argo-profiler (REASONED; argo): Server and repo-server pprof paths return 401 unless the profiler file contains exactly true with no newline; enabled profiling has no caller authentication. +- argo-profiler-file (REASONED; argo): ARGOCD_ENABLE_PROFILER_FILE_PATH defaults to /home/argocd/params/profiler.enabled; the server mounts ConfigMap key server.profile.enabled there. +- repo-grpc (REASONED; argo): Repo-server gRPC binds 0.0.0.0:8081 with TLS by default; mTLS is skipped without the client CA, separate from the serving certificate. +- repo-http (REASONED; argo): Repo-server HTTP metrics and health bind 0.0.0.0:8084 without an authentication wrapper. +- repo-limits (REASONED; argo): Repo-server requires verified client certificates with a loaded client CA; missing or unstatable serving files trigger a session self-signed certificate, while an invalid present pair errors. No authorization interceptor appears; service method authorization is unverified. +- controller-metrics (REASONED; argo): Application-controller metrics and health use 8082; the command passes that port to the controller, but the fetched files do not establish the bind address. +- applicationset-webhook (REASONED; argo): ApplicationSet webhook binds :7000; its container and Service declare 7000. +- applicationset-metrics (REASONED; argo): ApplicationSet metrics bind :8080; its container and Service declare 8080. +- applicationset-health (REASONED; argo): ApplicationSet health probes bind :8081 without a container or Service port declaration; missing declarations do not close sockets. +- notifications (REASONED; argo): Notifications HTTP metrics bind 0.0.0.0:9001 without authentication; a metrics Service and TCP liveness probe exist without containerPort. +- dex-http (REASONED; argo): Dex starts only with nonempty generated configuration; HTTP defaults to TLS on 0.0.0.0:5556 using /tmp/tls.crt and /tmp/tls.key. +- dex-grpc (REASONED; argo, dex): Generated Dex gRPC uses 0.0.0.0:5557 without TLS or client-auth settings; Dex v2.45.1 source sets no transport credentials or authentication interceptor for this configuration, while API method authorization is unverified; this assumes the deployed ghcr.io/dexidp/dex:v2.45.1 tag was built from Dex v2.45.1. +- dex-telemetry (REASONED; argo): Dex telemetry uses HTTP on 0.0.0.0:5558; all three Dex ports have Services. +- redis-password (REASONED; argo, redis-image, redis-source): Redis receives --requirepass from argocd-redis/auth; secret-init generates and checks that Secret. The redis:8.2.3-alpine tag, taken as built from docker-library-redis v8.2.3, uses Redis wildcard bind defaults and patches protected mode off. +- redis-tls (REASONED; argo): Base manifests configure no Redis TLS and the Argo Redis client defaults --redis-use-tls=false; protect traffic, Secret and cache. +- component-policies (REASONED; argo): Base policies restrict repo gRPC, Redis and Dex callers by component; enforcement needs a supporting CNI. +- broad-policies (REASONED; argo): Server policy allows all ingress; other metrics and ApplicationSet webhook allowances admit every namespace. Narrow existing additive allowances and egress separately. +- argo-anonymous (REASONED; argo, argo-docs): users.anonymous.enabled defaults off; anonymous callers otherwise inherit policy.default. +- argo-disable-auth (REASONED; argo): server.disable.auth defaults false; inspect effective args and environment because anonymous-off does not compensate for disabled authentication. +- argo-default-role (REASONED; argo, argo-docs): Base RBAC ConfigMap omits policy.default; the inspected loader supplies an empty role to the enforcer, which skips its additive default-role check. Default-role grants are additive and cannot be revoked by a later subject rule; define scoped policy.csv roles and group mappings. Server-side wiring remains unverified. +- argo-webhook (REASONED; argo-docs): /api/webhook accepts unauthenticated refresh events without a shared secret; configure one to limit spoofed reconciliation and resource exhaustion. +- argo-projects (REASONED; argo-docs): Restrict AppProjects including the built-in default project; project constraints do not reduce a compromised controller's Kubernetes credentials. +- argo-cluster-rbac (REASONED; argo-docs): Reduce controller and registered-cluster argocd-manager write permissions to needed namespaces/resources while retaining required read access. +- flux-api (REASONED; flux): Flux has no standalone management login/API; Kubernetes authenticates its custom-resource operations, while separate dashboards need their own controls. +- flux-bootstrap (REASONED; flux): GitHub bootstrap --token-auth retains a PAT in flux-system; --token-auth=false creates a read-only SSH deploy key by default. Protect bootstrap input and Secrets. +- flux-writeback (REASONED; flux): Image automation needs Git write access, including --read-write-key=true for the bootstrap key; scope the repository and protect update branches and objects. +- flux-artifacts (REASONED; flux): Source-controller serves fetched artifacts over internal HTTP; enforced NetworkPolicy, not ClusterIP alone, isolates them. +- flux-receiver-port (REASONED; flux): Webhook receiver listens on 9292 behind Service port 80; external exposure needs a route and TLS termination. +- flux-receiver-types (REASONED; flux): generic validates nothing; generic-hmac, GitHub, Bitbucket and Nexus use HMAC, while gitlab compares X-Gitlab-Token. +- flux-oidc (REASONED; flux, flux-oidc): generic-oidc, introduced in Flux 2.9, rejects secretRef and validates bearer tokens; constrain issuer validations and audience, whose default is notification-controller. +- flux-rbac (REASONED; flux): Only kustomize-controller and helm-controller hold cluster-admin; other controllers still have meaningful permissions including Secret access. +- flux-lockdown (REASONED; flux): Use scoped reconciliation ServiceAccounts, --no-cross-namespace-refs and --no-remote-bases; --default-service-account fills an omission, not an explicit serviceAccountName. +- verify-argo (REASONED; argo-docs, argo, curl): Anonymous application listing should fail with 401 while a valid token lists the expected application; repeat at origin and CLI/gRPC and inspect effective RBAC. +- verify-bootstrap (REASONED; argo-docs): Absence of the bootstrap Secret proves removal of that material, not password rotation. +- verify-inventory (REASONED; argo, flux): Inventory Services, routes, host ports, policies and component sockets; node-local ss and policy objects alone do not establish cluster isolation. +- verify-repo-metrics (REASONED; argo): Base policy permits HTTP 200 metrics to monitoring and unrelated pods; narrowed policy should admit only monitoring, with a successful positive control. +- verify-server-metrics (REASONED; argo): Repeat on server 8083: only authorized monitoring should receive metrics after narrowing ingress; a failed control is inconclusive. +- verify-profiler (REASONED; argo): From an allowed pod, pprof cmdline should return 401 disabled, 200 for exact true, then 401 for true plus newline; keep metrics as control. +- verify-flux (REASONED; flux, python, curl): Identical generic-hmac bodies without and with X-Signature should be rejected and accepted with reconciliation; status is type/version dependent. The cited curl curl-8_14_1 lines show --data-binary posting data exactly as specified, preserving newlines and carriage returns that --data strips from @file input, reading @- from stdin and @filename from a file, and posting like --data's POST request. +- verify-receiver-status (REASONED; flux): Read receiver name and Ready condition without exposing webhookPath or condition messages; origin checks distinguish receiver rejection from proxy rejection. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "908b108054ff323b7d30f6ec6652728a0d065571804b124943b579b8f09f77cc", + "components": { + "argo": { + "name": "Argo CD v3.5.3 source", + "basis": "c9c369efcc5b2a0bd720803f8d14a1c3eaddf579", + "sources": { + "sddf4853ad503": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-service.yaml#L9-L20", + "s6e8c96d23e05": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-server/commands/argocd_server.go#L307-L323", + "s96065cc09776": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/server.go#L512-L535", + "s7cc690d1bf69": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/server.go#L668-L677", + "s028a4769d6c3": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/metrics/metrics.go#L74-L100", + "sc52bbd14bff4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/profile/profile.go#L11-L30", + "s98b27313f615": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-repo-server/commands/argocd_repo_server.go#L180-L212", + "sb0648e746fb9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L406-L408", + "s2f5af86c62d1": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L475-L481", + "s9bca471ac6d9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-repo-server/commands/argocd_repo_server.go#L257-L280", + "s4bbc848d9923": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/repo-server/argocd-repo-server-deployment.yaml#L367-L384", + "sf98b6bc4f5dc": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/application-controller/argocd-application-controller-statefulset.yaml#L360-L365", + "sf4dd846c82c4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/common/common.go#L75-L92", + "s49a7c83d9371": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-applicationset-controller/commands/applicationset_controller.go#L294-L296", + "s97a588d8a83f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/applicationset-controller/argocd-applicationset-controller-service.yaml#L9-L20", + "s0a85b38dfca8": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-notification/commands/argocd_notification.go#L149-L154", + "s5e141e32ed9c": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/notification/argocd-notifications-controller-metrics-service.yaml#L9-L16", + "s8efcc66f9cb4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-dex/commands/argocd_dex.go#L82-L119", + "s30c391d9dc23": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-dex/commands/argocd_dex.go#L143-L146", + "se477de2fdfa0": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/dex/config.go#L16-L152", + "s56c41c02ba8b": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-service.yaml#L9-L25", + "s996c72c5db28": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/redis/argocd-redis-deployment.yaml#L18-L58", + "s34909d7a1932": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/cache/cache.go#L233-L238", + "s1a13cc06db6d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/repo-server/argocd-repo-server-network-policy.yaml#L9-L35", + "s4bbf38e83770": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/redis/argocd-redis-network-policy.yaml#L9-L28", + "sadccbbcffcd8": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-network-policy.yaml#L9-L29", + "s743c8187cbd7": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-network-policy.yaml#L9-L16", + "s49f63c21c3cd": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/application-controller/argocd-application-controller-network-policy.yaml#L9-L19", + "sa258c4109d24": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/applicationset-controller/argocd-applicationset-controller-network-policy.yaml#L9-L22", + "sa49c30e332ed": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/notification/argocd-notifications-controller-network-policy.yaml#L9-L20", + "s68b67007da6b": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/settings/settings.go#L1631-L1638", + "s01a52daf078f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L121-L126", + "s210b24f04535": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/config/argocd-cmd-params-cm.yaml#L1-L7", + "s0f7f1b3404d9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/config/argocd-rbac-cm.yaml#L1-L7", + "s85445da68ce7": "https://github.com/argoproj/argo-cd/blob/v3.5.3/common/common.go#L89-L90", + "s357d253fc789": "https://github.com/argoproj/argo-cd/blob/v3.5.3/cmd/argocd-repo-server/commands/argocd_repo_server.go#L257-L260", + "s9df1243dde48": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-application-controller/commands/argocd_application_controller.go#L192-L209", + "s0abbc26d61e4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-application-controller/commands/argocd_application_controller.go#L272-L273", + "sfcaf2e03154d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/reposerver/server.go#L49-L85", + "s25a5da2c5e7c": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/reposerver/server.go#L94-L150", + "s73653423b85d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/tls/tls.go#L428-L506", + "s7c7bf274e3ef": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-deployment.yaml#L38-L42", + "s08939ffb906f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd/commands/admin/redis_initial_password.go#L22-L89", + "s3fcdf1601776": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L454-L467", + "se987196f20c7": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L539-L547", + "s3cb98aa5348d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L399-L425", + "s9bfcf07a6bcc": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L314-L346", + "s32323471d785": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L35-L42" + } + }, + "flux-oidc": { + "name": "Flux OIDC introduction", + "basis": "v2.9", + "sources": { + "s1ea193b2b3cc": "https://fluxcd.io/blog/2026/06/flux-v2.9.0/" + } + }, + "argo-docs": { + "name": "Argo CD documentation", + "basis": "unknown", + "sources": { + "s51e21b74ece6": "https://argo-cd.readthedocs.io/en/stable/operator-manual/security/", + "s02463a4fb268": "https://argo-cd.readthedocs.io/en/stable/operator-manual/secret-management/", + "s2efb9ee633af": "https://argo-cd.readthedocs.io/en/stable/getting_started/", + "sc2590df86f4e": "https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/", + "sf5f1c41f89bd": "https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/", + "s3767d400e364": "https://argo-cd.readthedocs.io/en/stable/operator-manual/ingress/", + "sdac90731b97d": "https://argo-cd.readthedocs.io/en/stable/operator-manual/webhook/", + "s9357e3b59e84": "https://argo-cd.readthedocs.io/en/stable/user-guide/projects/" + } + }, + "flux": { + "name": "Flux documentation", + "basis": "unknown", + "sources": { + "s493859f3b848": "https://fluxcd.io/flux/security/best-practices/", + "s4dcd744a2444": "https://fluxcd.io/flux/guides/mozilla-sops/", + "sd8618bbbc628": "https://fluxcd.io/flux/components/source/ocirepositories/#secret-reference", + "sf3f80d9941c8": "https://fluxcd.io/flux/installation/configuration/multitenancy/", + "s0669b973542c": "https://fluxcd.io/flux/security/#controller-permissions", + "s13bd7f0204b0": "https://fluxcd.io/flux/installation/bootstrap/github/", + "s8759854c1bdf": "https://fluxcd.io/flux/components/image/imageupdateautomations/", + "s0acf42beed45": "https://fluxcd.io/flux/guides/webhook-receivers/", + "s35bb9a844dcf": "https://fluxcd.io/flux/components/notification/receivers/" + } + }, + "dex": { + "name": "Dex v2.45.1 source", + "basis": "v2.45.1", + "sources": { + "s3b9512b79dea": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/serve.go#L151-L199", + "s46eb2feaa84f": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/serve.go#L553-L572", + "s55b453f266f3": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/config.go#L255-L265" + } + }, + "redis-image": { + "name": "docker-library-redis v8.2.3 source", + "basis": "bcbc692165ce2241e194035041c5b5e01d2e1a5f", + "sources": { + "s67a2f7553c9a": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/Dockerfile#L81-L87", + "s30643c91686d": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/Dockerfile#L151-L154", + "sd04db8453288": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/docker-entrypoint.sh#L93-L111" + } + }, + "redis-source": { + "name": "Redis 8.2.3 source", + "basis": "8.2.3", + "sources": { + "s667fc16bf18f": "https://github.com/redis/redis/blob/8.2.3/src/server.h#L142-L143", + "saa497d27f72f": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2199-L2221", + "s0f96763525b7": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2638-L2673", + "s9b50a7c52721": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2983-L2996" + } + }, + "curl": { + "name": "curl", + "basis": "curl-8_14_1", + "sources": { + "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md", + "s9537bb790553": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data-binary.md#L4-L32", + "sd3b039bafbf6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data.md#L4-L49" + } + }, + "python": { + "name": "Python documentation", + "basis": "unknown", + "sources": { + "s7a54556e5dac": "https://docs.python.org/3/library/hmac.html", + "s31fccd7d50e5": "https://docs.python.org/3/library/sys.html#sys.stdin" + } + } + }, + "claims": { + "git-boundary": {"text": "Repository writers exercise reconciliation permissions; review platform changes, separate tenants and scope credentials.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s51e21b74ece6", "flux:s493859f3b848"], "status": "REASONED"}, + "secret-storage": {"text": "Protect bootstrap, registry, cluster, webhook and decryption credentials; commit only encrypted secrets and keep SOPS/age keys out of Git.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s02463a4fb268", "flux:s4dcd744a2444", "flux:sd8618bbbc628"], "status": "REASONED"}, + "egress": {"text": "Restrict fetch, API, identity and notification egress; Argo repository allowlists do not constrain remote bases or Helm dependencies.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s51e21b74ece6", "flux:sf3f80d9941c8"], "status": "REASONED"}, + "argo-bootstrap": {"text": "Rotate the initial admin password before deleting argocd-initial-admin-secret; deletion alone does not rotate it.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:sc2590df86f4e"], "status": "REASONED"}, + "argo-sso": {"text": "The built-in admin is unrestricted; configure named SSO roles and IdP MFA, then set admin.enabled=false.", "components": ["argo-docs"], "sources": ["argo-docs:sc2590df86f4e", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, + "argo-origin": {"text": "Keep the ClusterIP origin private behind authenticated TLS and protect both REST/browser and CLI/gRPC routes.", "components": ["argo-docs", "argo"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:s3767d400e364", "argo:sddf4853ad503"], "status": "REASONED"}, + "argo-server": {"text": "Server defaults to 0.0.0.0:8080; the Service maps 80 and 443 to it.", "components": ["argo"], "sources": ["argo:s6e8c96d23e05", "argo:sddf4853ad503", "argo:sf4dd846c82c4"], "status": "REASONED"}, + "argo-tls": {"text": "Server --insecure/server.insecure defaults false and disables backend TLS, not authentication; use only behind protected TLS termination.", "components": ["argo", "argo-docs"], "sources": ["argo:s6e8c96d23e05", "argo-docs:s3767d400e364"], "status": "REASONED"}, + "argo-client-tls": {"text": "argocd login --insecure skips client certificate verification; the server initially uses a self-signed certificate.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:s3767d400e364"], "status": "REASONED"}, + "argo-metrics": {"text": "Server HTTP metrics on 8083 are unauthenticated and bind to --address, not --metrics-address.", "components": ["argo"], "sources": ["argo:s96065cc09776", "argo:s7cc690d1bf69", "argo:s028a4769d6c3", "argo:sf4dd846c82c4", "argo:s6e8c96d23e05"], "status": "REASONED"}, + "argo-profiler": {"text": "Server and repo-server pprof paths return 401 unless the profiler file contains exactly true with no newline; enabled profiling has no caller authentication.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:s028a4769d6c3", "argo:s98b27313f615"], "status": "REASONED"}, + "argo-profiler-file": {"text": "ARGOCD_ENABLE_PROFILER_FILE_PATH defaults to /home/argocd/params/profiler.enabled; the server mounts ConfigMap key server.profile.enabled there.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:sb0648e746fb9", "argo:s2f5af86c62d1"], "status": "REASONED"}, + "repo-grpc": {"text": "Repo-server gRPC binds 0.0.0.0:8081 with TLS by default; mTLS is skipped without the client CA, separate from the serving certificate.", "components": ["argo"], "sources": ["argo:s9bca471ac6d9", "argo:s4bbc848d9923", "argo:sf4dd846c82c4", "argo:s85445da68ce7", "argo:s357d253fc789"], "status": "REASONED"}, + "repo-http": {"text": "Repo-server HTTP metrics and health bind 0.0.0.0:8084 without an authentication wrapper.", "components": ["argo"], "sources": ["argo:s98b27313f615", "argo:s9bca471ac6d9", "argo:sf4dd846c82c4", "argo:s85445da68ce7", "argo:s357d253fc789"], "status": "REASONED"}, + "repo-limits": {"text": "Repo-server requires verified client certificates with a loaded client CA; missing or unstatable serving files trigger a session self-signed certificate, while an invalid present pair errors. No authorization interceptor appears; service method authorization is unverified.", "components": ["argo"], "sources": ["argo:sfcaf2e03154d", "argo:s25a5da2c5e7c", "argo:s73653423b85d"], "status": "REASONED"}, + "controller-metrics": {"text": "Application-controller metrics and health use 8082; the command passes that port to the controller, but the fetched files do not establish the bind address.", "components": ["argo"], "sources": ["argo:sf98b6bc4f5dc", "argo:s9df1243dde48", "argo:s0abbc26d61e4", "argo:sf4dd846c82c4"], "status": "REASONED"}, + "applicationset-webhook": {"text": "ApplicationSet webhook binds :7000; its container and Service declare 7000.", "components": ["argo"], "sources": ["argo:s49a7c83d9371", "argo:s97a588d8a83f"], "status": "REASONED"}, + "applicationset-metrics": {"text": "ApplicationSet metrics bind :8080; its container and Service declare 8080.", "components": ["argo"], "sources": ["argo:s49a7c83d9371", "argo:s97a588d8a83f"], "status": "REASONED"}, + "applicationset-health": {"text": "ApplicationSet health probes bind :8081 without a container or Service port declaration; missing declarations do not close sockets.", "components": ["argo"], "sources": ["argo:s49a7c83d9371"], "status": "REASONED"}, + "notifications": {"text": "Notifications HTTP metrics bind 0.0.0.0:9001 without authentication; a metrics Service and TCP liveness probe exist without containerPort.", "components": ["argo"], "sources": ["argo:s0a85b38dfca8", "argo:s5e141e32ed9c"], "status": "REASONED"}, + "dex-http": {"text": "Dex starts only with nonempty generated configuration; HTTP defaults to TLS on 0.0.0.0:5556 using /tmp/tls.crt and /tmp/tls.key.", "components": ["argo"], "sources": ["argo:s8efcc66f9cb4", "argo:s30c391d9dc23", "argo:se477de2fdfa0"], "status": "REASONED"}, + "dex-grpc": {"text": "Generated Dex gRPC uses 0.0.0.0:5557 without TLS or client-auth settings; Dex v2.45.1 source sets no transport credentials or authentication interceptor for this configuration, while API method authorization is unverified; this assumes the deployed ghcr.io/dexidp/dex:v2.45.1 tag was built from Dex v2.45.1.", "components": ["argo", "dex"], "sources": ["argo:se477de2fdfa0", "argo:s56c41c02ba8b", "argo:s7c7bf274e3ef", "dex:s3b9512b79dea", "dex:s46eb2feaa84f", "dex:s55b453f266f3"], "status": "REASONED"}, + "dex-telemetry": {"text": "Dex telemetry uses HTTP on 0.0.0.0:5558; all three Dex ports have Services.", "components": ["argo"], "sources": ["argo:se477de2fdfa0", "argo:s56c41c02ba8b"], "status": "REASONED"}, + "redis-password": {"text": "Redis receives --requirepass from argocd-redis/auth; secret-init generates and checks that Secret. The redis:8.2.3-alpine tag, taken as built from docker-library-redis v8.2.3, uses Redis wildcard bind defaults and patches protected mode off.", "components": ["argo", "redis-image", "redis-source"], "sources": ["argo:s996c72c5db28", "argo:s08939ffb906f", "redis-image:s67a2f7553c9a", "redis-image:s30643c91686d", "redis-image:sd04db8453288", "redis-source:s667fc16bf18f", "redis-source:saa497d27f72f", "redis-source:s0f96763525b7", "redis-source:s9b50a7c52721"], "status": "REASONED"}, + "redis-tls": {"text": "Base manifests configure no Redis TLS and the Argo Redis client defaults --redis-use-tls=false; protect traffic, Secret and cache.", "components": ["argo"], "sources": ["argo:s996c72c5db28", "argo:s34909d7a1932"], "status": "REASONED"}, + "component-policies": {"text": "Base policies restrict repo gRPC, Redis and Dex callers by component; enforcement needs a supporting CNI.", "components": ["argo"], "sources": ["argo:s1a13cc06db6d", "argo:s4bbf38e83770", "argo:sadccbbcffcd8"], "status": "REASONED"}, + "broad-policies": {"text": "Server policy allows all ingress; other metrics and ApplicationSet webhook allowances admit every namespace. Narrow existing additive allowances and egress separately.", "components": ["argo"], "sources": ["argo:s743c8187cbd7", "argo:s49f63c21c3cd", "argo:sa258c4109d24", "argo:sa49c30e332ed"], "status": "REASONED"}, + "argo-anonymous": {"text": "users.anonymous.enabled defaults off; anonymous callers otherwise inherit policy.default.", "components": ["argo", "argo-docs"], "sources": ["argo:s68b67007da6b", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, + "argo-disable-auth": {"text": "server.disable.auth defaults false; inspect effective args and environment because anonymous-off does not compensate for disabled authentication.", "components": ["argo"], "sources": ["argo:s6e8c96d23e05", "argo:s01a52daf078f", "argo:s210b24f04535"], "status": "REASONED"}, + "argo-default-role": {"text": "Base RBAC ConfigMap omits policy.default; the inspected loader supplies an empty role to the enforcer, which skips its additive default-role check. Default-role grants are additive and cannot be revoked by a later subject rule; define scoped policy.csv roles and group mappings. Server-side wiring remains unverified.", "components": ["argo", "argo-docs"], "sources": ["argo:s0f7f1b3404d9", "argo:s3fcdf1601776", "argo:se987196f20c7", "argo:s3cb98aa5348d", "argo:s9bfcf07a6bcc", "argo:s32323471d785", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, + "argo-webhook": {"text": "/api/webhook accepts unauthenticated refresh events without a shared secret; configure one to limit spoofed reconciliation and resource exhaustion.", "components": ["argo-docs"], "sources": ["argo-docs:sdac90731b97d"], "status": "REASONED"}, + "argo-projects": {"text": "Restrict AppProjects including the built-in default project; project constraints do not reduce a compromised controller's Kubernetes credentials.", "components": ["argo-docs"], "sources": ["argo-docs:s9357e3b59e84", "argo-docs:s51e21b74ece6"], "status": "REASONED"}, + "argo-cluster-rbac": {"text": "Reduce controller and registered-cluster argocd-manager write permissions to needed namespaces/resources while retaining required read access.", "components": ["argo-docs"], "sources": ["argo-docs:s51e21b74ece6"], "status": "REASONED"}, + "flux-api": {"text": "Flux has no standalone management login/API; Kubernetes authenticates its custom-resource operations, while separate dashboards need their own controls.", "components": ["flux"], "sources": ["flux:s493859f3b848", "flux:s0669b973542c"], "status": "REASONED"}, + "flux-bootstrap": {"text": "GitHub bootstrap --token-auth retains a PAT in flux-system; --token-auth=false creates a read-only SSH deploy key by default. Protect bootstrap input and Secrets.", "components": ["flux"], "sources": ["flux:s13bd7f0204b0"], "status": "REASONED"}, + "flux-writeback": {"text": "Image automation needs Git write access, including --read-write-key=true for the bootstrap key; scope the repository and protect update branches and objects.", "components": ["flux"], "sources": ["flux:s13bd7f0204b0", "flux:s8759854c1bdf"], "status": "REASONED"}, + "flux-artifacts": {"text": "Source-controller serves fetched artifacts over internal HTTP; enforced NetworkPolicy, not ClusterIP alone, isolates them.", "components": ["flux"], "sources": ["flux:s493859f3b848"], "status": "REASONED"}, + "flux-receiver-port": {"text": "Webhook receiver listens on 9292 behind Service port 80; external exposure needs a route and TLS termination.", "components": ["flux"], "sources": ["flux:s0acf42beed45"], "status": "REASONED"}, + "flux-receiver-types": {"text": "generic validates nothing; generic-hmac, GitHub, Bitbucket and Nexus use HMAC, while gitlab compares X-Gitlab-Token.", "components": ["flux"], "sources": ["flux:s35bb9a844dcf"], "status": "REASONED"}, + "flux-oidc": {"text": "generic-oidc, introduced in Flux 2.9, rejects secretRef and validates bearer tokens; constrain issuer validations and audience, whose default is notification-controller.", "components": ["flux", "flux-oidc"], "sources": ["flux:s35bb9a844dcf", "flux-oidc:s1ea193b2b3cc"], "status": "REASONED"}, + "flux-rbac": {"text": "Only kustomize-controller and helm-controller hold cluster-admin; other controllers still have meaningful permissions including Secret access.", "components": ["flux"], "sources": ["flux:s0669b973542c"], "status": "REASONED"}, + "flux-lockdown": {"text": "Use scoped reconciliation ServiceAccounts, --no-cross-namespace-refs and --no-remote-bases; --default-service-account fills an omission, not an explicit serviceAccountName.", "components": ["flux"], "sources": ["flux:sf3f80d9941c8"], "status": "REASONED"}, + "verify-argo": {"text": "Anonymous application listing should fail with 401 while a valid token lists the expected application; repeat at origin and CLI/gRPC and inspect effective RBAC.", "components": ["argo-docs", "argo", "curl"], "sources": ["argo-docs:sf5f1c41f89bd", "argo:s6e8c96d23e05", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, + "verify-bootstrap": {"text": "Absence of the bootstrap Secret proves removal of that material, not password rotation.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af"], "status": "REASONED", "verify": [1]}, + "verify-inventory": {"text": "Inventory Services, routes, host ports, policies and component sockets; node-local ss and policy objects alone do not establish cluster isolation.", "components": ["argo", "flux"], "sources": ["argo:s743c8187cbd7", "flux:s493859f3b848"], "status": "REASONED", "verify": [1]}, + "verify-repo-metrics": {"text": "Base policy permits HTTP 200 metrics to monitoring and unrelated pods; narrowed policy should admit only monitoring, with a successful positive control.", "components": ["argo"], "sources": ["argo:s98b27313f615", "argo:s1a13cc06db6d"], "status": "REASONED", "verify": [2]}, + "verify-server-metrics": {"text": "Repeat on server 8083: only authorized monitoring should receive metrics after narrowing ingress; a failed control is inconclusive.", "components": ["argo"], "sources": ["argo:s028a4769d6c3", "argo:s743c8187cbd7"], "status": "REASONED"}, + "verify-profiler": {"text": "From an allowed pod, pprof cmdline should return 401 disabled, 200 for exact true, then 401 for true plus newline; keep metrics as control.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:s028a4769d6c3"], "status": "REASONED"}, + "verify-flux": {"text": "Identical generic-hmac bodies without and with X-Signature should be rejected and accepted with reconciliation; status is type/version dependent. The cited curl curl-8_14_1 lines show --data-binary posting data exactly as specified, preserving newlines and carriage returns that --data strips from @file input, reading @- from stdin and @filename from a file, and posting like --data's POST request.", "components": ["flux", "python", "curl"], "sources": ["flux:s35bb9a844dcf", "python:s7a54556e5dac", "python:s31fccd7d50e5", "curl:s3db522258e44", "curl:s9537bb790553", "curl:sd3b039bafbf6"], "status": "REASONED", "verify": [3]}, + "verify-receiver-status": {"text": "Read receiver name and Ready condition without exposing webhookPath or condition messages; origin checks distinguish receiver rejection from proxy rejection.", "components": ["flux"], "sources": ["flux:s35bb9a844dcf"], "status": "REASONED"} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/haproxy.md b/plugin/skills/secure-deployment-config/references/haproxy.md index 396dc915d..91505745d 100644 --- a/plugin/skills/secure-deployment-config/references/haproxy.md +++ b/plugin/skills/secure-deployment-config/references/haproxy.md @@ -1,142 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-05", - "documentation_checked": "2026-09", - "body_sha256": "afa78827cbab9d1fd7020200e3a50b28da524fc31cba3550243a014cbe4363a9", - "components": { - "haproxy": { - "name": "HAProxy", - "basis": "3.0", - "sources": { - "s5792e6ff4990": "https://docs.haproxy.org/3.0/configuration.html", - "s9c32621d6493": "https://docs.haproxy.org/3.0/management.html" - } - }, - "hsts": { - "name": "HAProxy http-after-response minimum", - "basis": "2.2+", - "sources": { - "s5792e6ff4990": "https://docs.haproxy.org/3.0/configuration.html" - } - }, - "docs": { - "name": "Supporting documentation", - "basis": "unknown", - "sources": { - "s2be90eb936bd": "https://www.haproxy.org/", - "sae992a8ac3a7": "https://www.haproxy.com/documentation/haproxy-configuration-tutorials/alerts-and-monitoring/statistics/", - "s192d42dd9feb": "https://www.authelia.com/integration/proxies/haproxy/", - "s529b0eabe2ed": "https://ssl-config.mozilla.org/" - } - }, - "openssl-cli": { - "name": "OpenSSL password command", - "basis": "3.0", - "sources": { - "s3b2ccc45875a": "https://docs.openssl.org/3.0/man1/openssl-passwd/" - } - }, - "certbot-source": { - "name": "Certbot deploy hook documentation", - "basis": "v2.3.0", - "sources": { - "sd39028a759a7": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L675" - } - }, - "cloudflare-source": { - "name": "Cloudflare Access MFA documentation", - "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", - "sources": { - "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" - } - } - }, - "claims": { - "scope": {"text": "Examples target a maintained HAProxy 3.0 installation and treat frontend, stats, runtime API and direct backend as separate surfaces.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493", "docs:s2be90eb936bd"], "status": "REASONED"}, - "pem": {"text": "Use matching chain/key in a combined PEM, built via a root-only temporary file and rename; separate key loading is also supported.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "renewal": {"text": "Rebuild the combined PEM and reload HAProxy after successful renewal using Certbot v2.3.0 --deploy-hook or an executable in /etc/letsencrypt/renewal-hooks/deploy (if the configuration directory is /etc/letsencrypt; certbot renew only).", "components": ["haproxy", "certbot-source"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493", "certbot-source:sd39028a759a7"], "status": "REASONED"}, - "tls": {"text": "Frontend :443 loads the PEM with ssl and ssl-min-ver TLSv1.2; :80 redirects to HTTPS with 301.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "ciphers": {"text": "ssl-default-bind-ciphers governs TLS 1.2 and -ciphersuites TLS 1.3; Mozilla is policy guidance, not HAProxy syntax authority.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:s529b0eabe2ed"], "status": "REASONED"}, - "privileges": {"text": "Preserve service hardening; root-started workers drop to haproxy user/group and chroot into a provisioned root-owned directory.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "timeouts": {"text": "Set connect 5s, client/server 30s and http-request 10s timeouts.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "hsts": {"text": "http-after-response (HAProxy 2.2+) covers generated 401/413 responses that http-response misses; includeSubDomains requires HTTPS on every affected subdomain.", "components": ["haproxy", "hsts"], "sources": ["haproxy:s5792e6ff4990", "hsts:s5792e6ff4990"], "status": "REASONED"}, - "backend": {"text": "Example backend is plaintext 127.0.0.1:3000 with health checks; keep it private.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "remote-backend": {"text": "Remote TLS backends require certificate verification, a trusted CA and verifyhost matching the backend name.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "basic": {"text": "Use a crypt(3)-hashed userlist and http_auth condition; $6$ works on glibc Linux, while insecure-password stores plaintext.", "components": ["haproxy", "openssl-cli"], "sources": ["haproxy:s5792e6ff4990", "openssl-cli:s3b2ccc45875a"], "status": "REASONED"}, - "mtls": {"text": "For machine access, bind verify required with ca-file requires trusted client certificates.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "mfa": {"text": "Basic is single-factor; Authelia uses its HAProxy Lua integration, or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization).", "components": ["docs", "cloudflare-source"], "sources": ["docs:s192d42dd9feb", "cloudflare-source:sdcac732541da"], "status": "REASONED"}, - "maxconn": {"text": "Global maxconn 4096 caps per-process concurrent connections; frontend maxconn 2000 caps that frontend.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "body-guard": {"text": "No single request-body-size directive: req.body_size reads advertised Content-Length; deny_status 413 rejects values above 10485760 without buffering.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "body-buffering": {"text": "Do not add http-buffer-request for this guard: it waits for a complete body or full request buffer.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "chunked": {"text": "Chunked req.body_size reflects available data, so this unbuffered guard does not meaningfully bound chunked uploads; enforce the real limit in the app.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, - "stats-default": {"text": "Stats is disabled until configured and enabling it adds no authentication; inventory every loaded config.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, - "stats-admin": {"text": "stats admin permits backend enable/disable/drain; omit it for monitoring and keep any admin page private.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, - "stats-auth": {"text": "Bind the example stats listener to TLS 127.0.0.1:8404 at /stats and use hashed-userlist auth instead of cleartext stats auth.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, - "runtime-default": {"text": "Runtime API is not enabled by default; default command level is operator, and level admin grants authority rather than authentication.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED"}, - "runtime-isolation": {"text": "Keep /run/haproxy/admin.sock a root-owned mode-600 Unix socket; HTTP stats auth does not protect an exposed runtime TCP socket.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED"}, - "verify-config": {"text": "haproxy -c precedes reload; the guide records shell checks but no HAProxy runtime or exposed/fixed response demonstration.", "components": ["haproxy"], "sources": ["haproxy:s9c32621d6493"], "status": "REASONED", "verify": [1]}, - "verify-redirect": {"text": "HTTP must return 301 with an HTTPS Location; transport/TLS/DNS errors are inconclusive.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, - "verify-auth": {"text": "On one protected resource, absent and wrong credentials yield 401 while valid credentials reach the expected app response; credentials travel via stdin.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, - "verify-size": {"text": "With valid credentials, 1M succeeds and 11M returns 413; remove only the deny rule in isolation to attribute refusal. These Content-Length probes do not test chunked bodies.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, - "verify-backend": {"text": "Inventory all listeners: backend 3000 must be loopback-only even if frontend checks pass, and the runtime endpoint must not be TCP.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED", "verify": [1]}, - "verify-stats": {"text": "Probe the 8404 stats listener itself via loopback with the certificate hostname: no credentials refused, valid credentials return statistics.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED", "verify": [2]}, - "verify-mtls": {"text": "With verify required, no/untrusted client certificates must fail TLS and trusted ones pass; readable cert/key controls avoid local errors, and Basic may still return 401.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [2]}, - "verify-runtime": {"text": "Check root/root mode 600 and root show info success versus nobody permission denial; reported admin command level is not proof of access control.", "components": ["haproxy"], "sources": ["haproxy:s9c32621d6493", "haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [2]} - } -} ---- # HAProxy: TLS termination and authentication - -**Version basis** +Written against HAProxy 3.0, HAProxy http-after-response minimum 2.2+, OpenSSL password command 3.0, and Certbot deploy hook documentation v2.3.0; 1 further source is pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-10-05; documentation checked September 2026. The guide records 30 claims, all reasoned from cited sources. -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-05; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- haproxy: HAProxy (3.0) -- hsts: HAProxy http-after-response minimum (2.2+) -- docs: Supporting documentation (unknown) -- openssl-cli: OpenSSL password command (3.0) -- certbot-source: Certbot deploy hook documentation (v2.3.0) -- cloudflare-source: Cloudflare Access MFA documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) - -Claims (full source mapping in the front matter): - -- scope (REASONED; haproxy, docs): Examples target a maintained HAProxy 3.0 installation and treat frontend, stats, runtime API and direct backend as separate surfaces. -- pem (REASONED; haproxy): Use matching chain/key in a combined PEM, built via a root-only temporary file and rename; separate key loading is also supported. -- renewal (REASONED; haproxy, certbot-source): Rebuild the combined PEM and reload HAProxy after successful renewal using Certbot v2.3.0 --deploy-hook or an executable in /etc/letsencrypt/renewal-hooks/deploy (if the configuration directory is /etc/letsencrypt; certbot renew only). -- tls (REASONED; haproxy): Frontend :443 loads the PEM with ssl and ssl-min-ver TLSv1.2; :80 redirects to HTTPS with 301. -- ciphers (REASONED; haproxy, docs): ssl-default-bind-ciphers governs TLS 1.2 and -ciphersuites TLS 1.3; Mozilla is policy guidance, not HAProxy syntax authority. -- privileges (REASONED; haproxy): Preserve service hardening; root-started workers drop to haproxy user/group and chroot into a provisioned root-owned directory. -- timeouts (REASONED; haproxy): Set connect 5s, client/server 30s and http-request 10s timeouts. -- hsts (REASONED; haproxy, hsts): http-after-response (HAProxy 2.2+) covers generated 401/413 responses that http-response misses; includeSubDomains requires HTTPS on every affected subdomain. -- backend (REASONED; haproxy): Example backend is plaintext 127.0.0.1:3000 with health checks; keep it private. -- remote-backend (REASONED; haproxy): Remote TLS backends require certificate verification, a trusted CA and verifyhost matching the backend name. -- basic (REASONED; haproxy, openssl-cli): Use a crypt(3)-hashed userlist and http_auth condition; $6$ works on glibc Linux, while insecure-password stores plaintext. -- mtls (REASONED; haproxy): For machine access, bind verify required with ca-file requires trusted client certificates. -- mfa (REASONED; docs, cloudflare-source): Basic is single-factor; Authelia uses its HAProxy Lua integration, or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization). -- maxconn (REASONED; haproxy): Global maxconn 4096 caps per-process concurrent connections; frontend maxconn 2000 caps that frontend. -- body-guard (REASONED; haproxy): No single request-body-size directive: req.body_size reads advertised Content-Length; deny_status 413 rejects values above 10485760 without buffering. -- body-buffering (REASONED; haproxy): Do not add http-buffer-request for this guard: it waits for a complete body or full request buffer. -- chunked (REASONED; haproxy): Chunked req.body_size reflects available data, so this unbuffered guard does not meaningfully bound chunked uploads; enforce the real limit in the app. -- stats-default (REASONED; haproxy, docs): Stats is disabled until configured and enabling it adds no authentication; inventory every loaded config. -- stats-admin (REASONED; haproxy, docs): stats admin permits backend enable/disable/drain; omit it for monitoring and keep any admin page private. -- stats-auth (REASONED; haproxy, docs): Bind the example stats listener to TLS 127.0.0.1:8404 at /stats and use hashed-userlist auth instead of cleartext stats auth. -- runtime-default (REASONED; haproxy): Runtime API is not enabled by default; default command level is operator, and level admin grants authority rather than authentication. -- runtime-isolation (REASONED; haproxy): Keep /run/haproxy/admin.sock a root-owned mode-600 Unix socket; HTTP stats auth does not protect an exposed runtime TCP socket. -- verify-config (REASONED; haproxy): haproxy -c precedes reload; the guide records shell checks but no HAProxy runtime or exposed/fixed response demonstration. -- verify-redirect (REASONED; haproxy): HTTP must return 301 with an HTTPS Location; transport/TLS/DNS errors are inconclusive. -- verify-auth (REASONED; haproxy): On one protected resource, absent and wrong credentials yield 401 while valid credentials reach the expected app response; credentials travel via stdin. -- verify-size (REASONED; haproxy): With valid credentials, 1M succeeds and 11M returns 413; remove only the deny rule in isolation to attribute refusal. These Content-Length probes do not test chunked bodies. -- verify-backend (REASONED; haproxy): Inventory all listeners: backend 3000 must be loopback-only even if frontend checks pass, and the runtime endpoint must not be TCP. -- verify-stats (REASONED; haproxy, docs): Probe the 8404 stats listener itself via loopback with the certificate hostname: no credentials refused, valid credentials return statistics. -- verify-mtls (REASONED; haproxy): With verify required, no/untrusted client certificates must fail TLS and trusted ones pass; readable cert/key controls avoid local errors, and Basic may still return 401. -- verify-runtime (REASONED; haproxy): Check root/root mode 600 and root show info success versus nobody permission denial; reported admin command level is not proof of access control. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. HAProxy's public frontend, its stats interface, its runtime API, and any directly reachable backend are separate exposure surfaces; protect and verify each one you enable. These examples target a maintained HAProxy 3.0 installation. @@ -374,3 +240,163 @@ sudo -u nobody socat - /run/haproxy/admin.sock < /dev/null # unauthorize - OpenSSL 3.0 `passwd -6` SHA-512 crypt and terminal prompting (checked October 2026): https://docs.openssl.org/3.0/man1/openssl-passwd/ - Certbot v2.3.0 deploy hooks after successful renewal (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L675 - Cloudflare Access MFA documentation at c896795b8e8bab68d05ae78d64c9cbcda1fdb104, application or policy MFA (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/haproxy.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- haproxy: HAProxy (3.0) +- hsts: HAProxy http-after-response minimum (2.2+) +- docs: Supporting documentation (unknown) +- openssl-cli: OpenSSL password command (3.0) +- certbot-source: Certbot deploy hook documentation (v2.3.0) +- cloudflare-source: Cloudflare Access MFA documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) + +Claims (full source mapping in the JSON block below): + +- scope (REASONED; haproxy, docs): Examples target a maintained HAProxy 3.0 installation and treat frontend, stats, runtime API and direct backend as separate surfaces. +- pem (REASONED; haproxy): Use matching chain/key in a combined PEM, built via a root-only temporary file and rename; separate key loading is also supported. +- renewal (REASONED; haproxy, certbot-source): Rebuild the combined PEM and reload HAProxy after successful renewal using Certbot v2.3.0 --deploy-hook or an executable in /etc/letsencrypt/renewal-hooks/deploy (if the configuration directory is /etc/letsencrypt; certbot renew only). +- tls (REASONED; haproxy): Frontend :443 loads the PEM with ssl and ssl-min-ver TLSv1.2; :80 redirects to HTTPS with 301. +- ciphers (REASONED; haproxy, docs): ssl-default-bind-ciphers governs TLS 1.2 and -ciphersuites TLS 1.3; Mozilla is policy guidance, not HAProxy syntax authority. +- privileges (REASONED; haproxy): Preserve service hardening; root-started workers drop to haproxy user/group and chroot into a provisioned root-owned directory. +- timeouts (REASONED; haproxy): Set connect 5s, client/server 30s and http-request 10s timeouts. +- hsts (REASONED; haproxy, hsts): http-after-response (HAProxy 2.2+) covers generated 401/413 responses that http-response misses; includeSubDomains requires HTTPS on every affected subdomain. +- backend (REASONED; haproxy): Example backend is plaintext 127.0.0.1:3000 with health checks; keep it private. +- remote-backend (REASONED; haproxy): Remote TLS backends require certificate verification, a trusted CA and verifyhost matching the backend name. +- basic (REASONED; haproxy, openssl-cli): Use a crypt(3)-hashed userlist and http_auth condition; $6$ works on glibc Linux, while insecure-password stores plaintext. +- mtls (REASONED; haproxy): For machine access, bind verify required with ca-file requires trusted client certificates. +- mfa (REASONED; docs, cloudflare-source): Basic is single-factor; Authelia uses its HAProxy Lua integration, or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization). +- maxconn (REASONED; haproxy): Global maxconn 4096 caps per-process concurrent connections; frontend maxconn 2000 caps that frontend. +- body-guard (REASONED; haproxy): No single request-body-size directive: req.body_size reads advertised Content-Length; deny_status 413 rejects values above 10485760 without buffering. +- body-buffering (REASONED; haproxy): Do not add http-buffer-request for this guard: it waits for a complete body or full request buffer. +- chunked (REASONED; haproxy): Chunked req.body_size reflects available data, so this unbuffered guard does not meaningfully bound chunked uploads; enforce the real limit in the app. +- stats-default (REASONED; haproxy, docs): Stats is disabled until configured and enabling it adds no authentication; inventory every loaded config. +- stats-admin (REASONED; haproxy, docs): stats admin permits backend enable/disable/drain; omit it for monitoring and keep any admin page private. +- stats-auth (REASONED; haproxy, docs): Bind the example stats listener to TLS 127.0.0.1:8404 at /stats and use hashed-userlist auth instead of cleartext stats auth. +- runtime-default (REASONED; haproxy): Runtime API is not enabled by default; default command level is operator, and level admin grants authority rather than authentication. +- runtime-isolation (REASONED; haproxy): Keep /run/haproxy/admin.sock a root-owned mode-600 Unix socket; HTTP stats auth does not protect an exposed runtime TCP socket. +- verify-config (REASONED; haproxy): haproxy -c precedes reload; the guide records shell checks but no HAProxy runtime or exposed/fixed response demonstration. +- verify-redirect (REASONED; haproxy): HTTP must return 301 with an HTTPS Location; transport/TLS/DNS errors are inconclusive. +- verify-auth (REASONED; haproxy): On one protected resource, absent and wrong credentials yield 401 while valid credentials reach the expected app response; credentials travel via stdin. +- verify-size (REASONED; haproxy): With valid credentials, 1M succeeds and 11M returns 413; remove only the deny rule in isolation to attribute refusal. These Content-Length probes do not test chunked bodies. +- verify-backend (REASONED; haproxy): Inventory all listeners: backend 3000 must be loopback-only even if frontend checks pass, and the runtime endpoint must not be TCP. +- verify-stats (REASONED; haproxy, docs): Probe the 8404 stats listener itself via loopback with the certificate hostname: no credentials refused, valid credentials return statistics. +- verify-mtls (REASONED; haproxy): With verify required, no/untrusted client certificates must fail TLS and trusted ones pass; readable cert/key controls avoid local errors, and Basic may still return 401. +- verify-runtime (REASONED; haproxy): Check root/root mode 600 and root show info success versus nobody permission denial; reported admin command level is not proof of access control. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "afa78827cbab9d1fd7020200e3a50b28da524fc31cba3550243a014cbe4363a9", + "components": { + "haproxy": { + "name": "HAProxy", + "basis": "3.0", + "sources": { + "s5792e6ff4990": "https://docs.haproxy.org/3.0/configuration.html", + "s9c32621d6493": "https://docs.haproxy.org/3.0/management.html" + } + }, + "hsts": { + "name": "HAProxy http-after-response minimum", + "basis": "2.2+", + "sources": { + "s5792e6ff4990": "https://docs.haproxy.org/3.0/configuration.html" + } + }, + "docs": { + "name": "Supporting documentation", + "basis": "unknown", + "sources": { + "s2be90eb936bd": "https://www.haproxy.org/", + "sae992a8ac3a7": "https://www.haproxy.com/documentation/haproxy-configuration-tutorials/alerts-and-monitoring/statistics/", + "s192d42dd9feb": "https://www.authelia.com/integration/proxies/haproxy/", + "s529b0eabe2ed": "https://ssl-config.mozilla.org/" + } + }, + "openssl-cli": { + "name": "OpenSSL password command", + "basis": "3.0", + "sources": { + "s3b2ccc45875a": "https://docs.openssl.org/3.0/man1/openssl-passwd/" + } + }, + "certbot-source": { + "name": "Certbot deploy hook documentation", + "basis": "v2.3.0", + "sources": { + "sd39028a759a7": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L675" + } + }, + "cloudflare-source": { + "name": "Cloudflare Access MFA documentation", + "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" + } + } + }, + "claims": { + "scope": {"text": "Examples target a maintained HAProxy 3.0 installation and treat frontend, stats, runtime API and direct backend as separate surfaces.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493", "docs:s2be90eb936bd"], "status": "REASONED"}, + "pem": {"text": "Use matching chain/key in a combined PEM, built via a root-only temporary file and rename; separate key loading is also supported.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "renewal": {"text": "Rebuild the combined PEM and reload HAProxy after successful renewal using Certbot v2.3.0 --deploy-hook or an executable in /etc/letsencrypt/renewal-hooks/deploy (if the configuration directory is /etc/letsencrypt; certbot renew only).", "components": ["haproxy", "certbot-source"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493", "certbot-source:sd39028a759a7"], "status": "REASONED"}, + "tls": {"text": "Frontend :443 loads the PEM with ssl and ssl-min-ver TLSv1.2; :80 redirects to HTTPS with 301.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "ciphers": {"text": "ssl-default-bind-ciphers governs TLS 1.2 and -ciphersuites TLS 1.3; Mozilla is policy guidance, not HAProxy syntax authority.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:s529b0eabe2ed"], "status": "REASONED"}, + "privileges": {"text": "Preserve service hardening; root-started workers drop to haproxy user/group and chroot into a provisioned root-owned directory.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "timeouts": {"text": "Set connect 5s, client/server 30s and http-request 10s timeouts.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "hsts": {"text": "http-after-response (HAProxy 2.2+) covers generated 401/413 responses that http-response misses; includeSubDomains requires HTTPS on every affected subdomain.", "components": ["haproxy", "hsts"], "sources": ["haproxy:s5792e6ff4990", "hsts:s5792e6ff4990"], "status": "REASONED"}, + "backend": {"text": "Example backend is plaintext 127.0.0.1:3000 with health checks; keep it private.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "remote-backend": {"text": "Remote TLS backends require certificate verification, a trusted CA and verifyhost matching the backend name.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "basic": {"text": "Use a crypt(3)-hashed userlist and http_auth condition; $6$ works on glibc Linux, while insecure-password stores plaintext.", "components": ["haproxy", "openssl-cli"], "sources": ["haproxy:s5792e6ff4990", "openssl-cli:s3b2ccc45875a"], "status": "REASONED"}, + "mtls": {"text": "For machine access, bind verify required with ca-file requires trusted client certificates.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "mfa": {"text": "Basic is single-factor; Authelia uses its HAProxy Lua integration, or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization).", "components": ["docs", "cloudflare-source"], "sources": ["docs:s192d42dd9feb", "cloudflare-source:sdcac732541da"], "status": "REASONED"}, + "maxconn": {"text": "Global maxconn 4096 caps per-process concurrent connections; frontend maxconn 2000 caps that frontend.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "body-guard": {"text": "No single request-body-size directive: req.body_size reads advertised Content-Length; deny_status 413 rejects values above 10485760 without buffering.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "body-buffering": {"text": "Do not add http-buffer-request for this guard: it waits for a complete body or full request buffer.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "chunked": {"text": "Chunked req.body_size reflects available data, so this unbuffered guard does not meaningfully bound chunked uploads; enforce the real limit in the app.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "stats-default": {"text": "Stats is disabled until configured and enabling it adds no authentication; inventory every loaded config.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, + "stats-admin": {"text": "stats admin permits backend enable/disable/drain; omit it for monitoring and keep any admin page private.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, + "stats-auth": {"text": "Bind the example stats listener to TLS 127.0.0.1:8404 at /stats and use hashed-userlist auth instead of cleartext stats auth.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, + "runtime-default": {"text": "Runtime API is not enabled by default; default command level is operator, and level admin grants authority rather than authentication.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED"}, + "runtime-isolation": {"text": "Keep /run/haproxy/admin.sock a root-owned mode-600 Unix socket; HTTP stats auth does not protect an exposed runtime TCP socket.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED"}, + "verify-config": {"text": "haproxy -c precedes reload; the guide records shell checks but no HAProxy runtime or exposed/fixed response demonstration.", "components": ["haproxy"], "sources": ["haproxy:s9c32621d6493"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP must return 301 with an HTTPS Location; transport/TLS/DNS errors are inconclusive.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "On one protected resource, absent and wrong credentials yield 401 while valid credentials reach the expected app response; credentials travel via stdin.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, + "verify-size": {"text": "With valid credentials, 1M succeeds and 11M returns 413; remove only the deny rule in isolation to attribute refusal. These Content-Length probes do not test chunked bodies.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, + "verify-backend": {"text": "Inventory all listeners: backend 3000 must be loopback-only even if frontend checks pass, and the runtime endpoint must not be TCP.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED", "verify": [1]}, + "verify-stats": {"text": "Probe the 8404 stats listener itself via loopback with the certificate hostname: no credentials refused, valid credentials return statistics.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED", "verify": [2]}, + "verify-mtls": {"text": "With verify required, no/untrusted client certificates must fail TLS and trusted ones pass; readable cert/key controls avoid local errors, and Basic may still return 401.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [2]}, + "verify-runtime": {"text": "Check root/root mode 600 and root show info success versus nobody permission denial; reported admin command level is not proof of access control.", "components": ["haproxy"], "sources": ["haproxy:s9c32621d6493", "haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [2]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/headers.md b/plugin/skills/secure-deployment-config/references/headers.md index bf8efb707..2ec700c4e 100644 --- a/plugin/skills/secure-deployment-config/references/headers.md +++ b/plugin/skills/secure-deployment-config/references/headers.md @@ -1,5 +1,177 @@ ---- -version_basis: { +# Security headers for your application + +Written against MDN HTTP documentation (no recorded version), nginx source release-1.26.3, Apache HTTP Server 2.4.69, Caddy source v2.11.4, Helmet v8.1.0, Django 5.2, and curl 8.23.0; 1 further source is pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 24 claims, all reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + +TLS protects the transport; these response headers protect the page. Set them at the proxy ([`add_header`](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L110-L116) in [nginx.md](nginx.md), [`Header`](https://github.com/apache/httpd/blob/2.4.69/docs/manual/mod/mod_headers.xml#L311-L328) in [apache.md](apache.md), [`header`](https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/headers/caddyfile.go#L27-L48) in Caddy), in app middleware (helmet for Express per [nodejs.md](nodejs.md), Django's security settings per [python.md](python.md)), or in a [`_headers` file](https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L14) for Cloudflare Pages static asset responses. Cloudflare Pages does not apply `_headers` to Pages Functions responses; set those headers in [Functions code](https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L26). Applying the three proxy directives to all six headers below is this guide's inference. + +## The set worth shipping + +``` +Strict-Transport-Security: max-age=31536000; includeSubDomains +Content-Security-Policy: default-src 'self' +X-Content-Type-Options: nosniff +Referrer-Policy: strict-origin-when-cross-origin +Permissions-Policy: camera=(), microphone=(), geolocation=() +X-Frame-Options: DENY +``` + +Notes that keep these correct rather than decorative: + +- **HSTS** only after HTTPS provably works everywhere on the domain; `includeSubDomains` commits every subdomain to HTTPS. Leave the `preload` token off unless you have read what preload-list inclusion means: removal is possible but slow (weeks, and browser-dependent as each browser ships an updated list), so treat inclusion as a long-lived commitment. +- **CSP** is the one that needs tailoring. Start from `default-src 'self'`, add the sources your app actually uses, and prefer nonces or hashes over `'unsafe-inline'` for scripts. Roll out with `Content-Security-Policy-Report-Only` first on an existing app so you see what would break before enforcing. +- **frame-ancestors** in CSP supersedes `X-Frame-Options` in browsers that support it. The modern equivalent of `X-Frame-Options: DENY` is `frame-ancestors 'none'` in the CSP; add it explicitly, because `default-src` does not cover framing. Sending both `X-Frame-Options` and `frame-ancestors` keeps older scanners content. +- Headers belong on every response, including error pages; this guide assesses setting them only on `200 /` as a common proxy misconfiguration. In nginx, `add_header` inherits the parent's headers only if a child has no header list ([merge](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L665-L690); [list creation](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L788-L796)), and [`always`](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L843-L849) covers statuses outside its default set ([status handling](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L216-L218); [nginx.md](nginx.md)). + +## Do not cache authenticated responses in a shared cache + +A CDN or shared proxy that keys its cache on the URL alone can serve one user's authenticated response to another. Set `Cache-Control: private` on authenticated responses, or `no-store` on the most sensitive ones, and mark cacheable only what is truly public. If a shared cache must hold authenticated content, configure it explicitly to vary on the cookie or the authorization header rather than relying on its default URL-only key. + +## Keep authenticated pages out of the browser's own cache + +`private` stops a shared cache from holding a response, but it still lets the user's own browser store it: MDN defines `private` as a response the browser's local cache may keep. So the Back button can redisplay an authenticated page after logout, and a chat transcript can sit in the history of a shared machine. Set `Cache-Control: no-store` on any response that carries authenticated content or sets a session cookie; MDN defines `no-store` as "any caches of any kind (private or shared) should not store this response", which covers the browser too. + +On logout, clear what the browser already holds. `Cache-Control: no-cache` does not cover this, because a history navigation such as the Back button can restore a back/forward-cache snapshot without revalidating. Send the `Clear-Site-Data` header on the logout-confirmation response instead: its `cache` directive clears the browser cache and, depending on the browser, the back/forward cache as well, and `cookies` clears the client-side session cookie (invalidate the server-side session separately, as part of logout). MDN's own logout example is `Clear-Site-Data: "cache", "cookies", "storage", "executionContexts", "prefetchCache", "prerenderCache"`. Know the scope before sending the full set: `"cookies"` clears cookies across the entire registered domain (all subdomains) and also clears HTTP authentication credentials, and `"storage"` removes origin local/session storage, IndexedDB, and service-worker registrations, so a shared parent domain or a companion app on a subdomain is affected too. + +## Verify + +REASONED: final-response header inspection follows the cited MDN header documentation; curl 8.23.0 documents the command options and write-out fields, while `-D -` printing each response's header block, and selecting the last one, are this guide's assessment. No deployment URL was supplied for live checks, and this guide records no run; the following block states the expected values and treats failed fetches as inconclusive. + +```bash +# -q (first) skips curlrc, -g disables URL globbing, and -sS hides progress but shows errors. +# Fetch the FINAL page and read its response headers by eye. -L follows redirects (--proto-redir +# '=https' blocks an https-to-http downgrade) so you inspect the page a browser lands on, not a 302; +# --noproxy so no client proxy rewrites the headers; the body is discarded. -D - prints EVERY +# response's headers (a 103 Early Hints and each redirect hop, then the final one), and -w prints where +# you ended up, so read the block under the LAST "HTTP/.. " line: +curl -q -g -sS -L --proto-redir '=https' --noproxy '*' -D - -o /dev/null -w 'final: %{http_code} %{url_effective}\n' https://example.com/ +# In that final response check VALUES, not just presence: Strict-Transport-Security with a large, +# non-zero max-age (max-age=0 disables HSTS); an ENFORCING Content-Security-Policy, not only +# Content-Security-Policy-Report-Only (which enforces nothing); X-Content-Type-Options nosniff; +# Referrer-Policy strict-origin-when-cross-origin; Permissions-Policy denying camera, microphone, and +# geolocation; and an effective deny-framing policy: CSP frame-ancestors 'none', or X-Frame-Options +# DENY with NO enforcing frame-ancestors overriding it (an enforcing frame-ancestors always wins over +# X-Frame-Options, so frame-ancestors * with X-Frame-Options: DENY still allows framing). A header +# missing from the final response, or present only on an earlier hop, is a finding; a failed fetch is +# inconclusive. +``` + +REASONED: preceding block; final-response header values, external scanning and CSP discrimination, per the guide's cited MDN HTTP headers reference and Security header scanner. No deployed application or controlled browser application session is available, and network access is unavailable. bash -n and ShellCheck 0.11.0 exited 0 without diagnostics; no HTTP response or browser outcome was observed. Exposed responses omit effective controls or allow the forbidden resource; fixed final responses carry the stated values and block the forbidden resource while permitting the allowed control. Then scan with https://securityheaders.com/ from outside for a second opinion (it fetches with GET). No UNEXPECTED CSP console violations during normal application use is necessary but not sufficient: a permissive CSP produces none, and a correct policy still logs a violation when it blocks something (the negative test below, an attack, or an unwanted load). Confirm the policy actually blocks by loading a page with a deliberately disallowed resource (an inline script the policy forbids, or an off-origin image) and checking it is refused while an allowed resource still loads. + +REASONED: shared-cache caller separation, per the guide's cited MDN Cache-Control. No deployed shared cache or authenticated A/B accounts are available; this prose supplies no local command or configuration to execute. No response sequence was observed. Exposed caching replays a previous caller's private content; fixed responses after warming the same URL belong only to their current authenticated or anonymous caller. For an authenticated route behind a shared cache, request the same URL as user A, then as user B, then anonymously, after warming the cache; each response must reflect only its own caller, never the one before it. + +REASONED: logout, browser history and response-header inspection, per the guide's cited MDN Cache-Control, MDN Clear-Site-Data and MDN back/forward cache. No application session or supported-browser deployment matrix is available; this prose supplies no local command or configuration to execute. No logout sequence was observed. Exposed history restores authenticated content after logout; the expected fixed outcome is a new request reaching login, with no-store and Clear-Site-Data checked independently in every supported browser. In a real browser, log in, open an authenticated page, then log out and press the Back button: the authenticated page must not reappear from history. The browser should re-request it and land on the login screen. That confirms the logout outcome, not which header produced it, so also inspect the authenticated and logout responses for the intended `no-store` and `Clear-Site-Data` headers. Because the back/forward-cache clearing is browser-dependent, run the check in each browser you support. + +## Sources (checked September 2026) + +- MDN HTTP headers reference (Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, X-Frame-Options): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers +- MDN Cache-Control (`no-store` forbids any cache including the browser; `private` still permits the browser's local cache): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control +- MDN Clear-Site-Data (the `cache` directive clears the browser cache and, depending on the browser, the back/forward cache; the logout example): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Clear-Site-Data +- MDN back/forward cache (a history navigation restores a snapshot without revalidating): https://developer.mozilla.org/en-US/docs/Glossary/bfcache +- Security header scanner: https://securityheaders.com/ +- Helmet v8.1.0 response-header middleware (checked October 2026): https://github.com/helmetjs/helmet/blob/v8.1.0/README.md#L3 +- Django 5.2 SecurityMiddleware (checked October 2026): https://docs.djangoproject.com/en/5.2/ref/middleware/#module-django.middleware.security +- nginx release-1.26.3 add_header directive (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L110-L116 +- nginx release-1.26.3 add_header list creation (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L788-L796 +- nginx release-1.26.3 header inheritance (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L665-L690 +- nginx release-1.26.3 always status handling (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L216-L218 +- nginx release-1.26.3 always option (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L843-L849 +- Apache 2.4.69 Header response-header directive (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/docs/manual/mod/mod_headers.xml#L311-L328 +- Caddy v2.11.4 header response-header directive (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/headers/caddyfile.go#L27-L48 +- Cloudflare Pages _headers on static assets (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L14 +- Cloudflare Pages _headers Pages Functions limit (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L26 +- curl 8.23.0 --disable (first argument skips curlrc) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/disable.md#L18-L20 +- curl 8.23.0 --globoff (disables URL globbing) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/globoff.md#L19-L22 +- curl 8.23.0 --silent (quiet mode with --show-error) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/silent.md#L20-L25 +- curl 8.23.0 --show-error (reports errors with --silent) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/show-error.md#L19 +- curl 8.23.0 --location (follows redirects) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/location.md#L23-L26 +- curl 8.23.0 --proto-redir (restricts redirect protocols) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/proto-redir.md#L19-L20 +- curl 8.23.0 --proto (= permits only the named protocol) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/proto.md#L30-L33 +- curl 8.23.0 --noproxy (* disables proxy use) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/noproxy.md#L18-L23 +- curl 8.23.0 --dump-header (- writes headers to stdout) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/dump-header.md#L21-L23 +- curl 8.23.0 --output (/dev/null discards the body) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/output.md#L53-L55 +- curl 8.23.0 --write-out (format and variables) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L20-L29 +- curl 8.23.0 --write-out http_code (last response code) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L112-L114 +- curl 8.23.0 --write-out url_effective (last fetched URL) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L358-L360 +- curl 8.23.0 --disable short form -q (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/disable.md#L5 +- curl 8.23.0 --globoff short form -g (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/globoff.md#L5 +- curl 8.23.0 --silent short form -s (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/silent.md#L5 +- curl 8.23.0 --show-error short form -S (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/show-error.md#L5 +- curl 8.23.0 --location short form -L (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/location.md#L5 +- curl 8.23.0 --dump-header short form -D (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/dump-header.md#L5 +- curl 8.23.0 --output short form -o (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/output.md#L6 +- curl 8.23.0 --write-out short form -w (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L5 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/headers.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- mdn: MDN HTTP documentation (unknown) +- nginx-source: nginx source (release-1.26.3) +- apache-source: Apache HTTP Server (2.4.69) +- caddy-source: Caddy source (v2.11.4) +- cloudflare-pages: Cloudflare Pages documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- helmet: Helmet (v8.1.0) +- django: Django (5.2) +- scanner: Security Headers scanner (unknown) +- curl: curl (8.23.0) + +Claims (full source mapping in the JSON block below): + +- placement (REASONED; mdn, helmet, django, nginx-source, apache-source, caddy-source, cloudflare-pages): Set response headers at a proxy with nginx add_header, Apache Header or Caddy header, in application middleware, or in a Cloudflare Pages _headers file for static asset responses; Pages Functions responses will likely need headers set in Functions code. Applying the three proxy directives to all six listed headers is this guide's inference. +- hsts (REASONED; mdn): Use HSTS max-age=31536000 with includeSubDomains only after HTTPS works across the domain; every subdomain is committed to HTTPS and max-age=0 disables HSTS. +- hsts-preload (REASONED; mdn): Omit preload unless its commitment is understood; removal is slow, takes weeks and depends on browser list updates. +- csp (REASONED; mdn): Start CSP at default-src 'self', tailor sources to the app and prefer script nonces or hashes over unsafe-inline; Verify requires an enforcing policy. +- csp-report-only (REASONED; mdn): Roll out Content-Security-Policy-Report-Only first on existing apps to find breakage; it enforces nothing and is not a substitute for enforcing CSP. +- nosniff (REASONED; mdn): Send X-Content-Type-Options: nosniff and verify that value on the final response. +- referrer (REASONED; mdn): Send Referrer-Policy: strict-origin-when-cross-origin and verify that final-response value. +- permissions (REASONED; mdn): Permissions-Policy disables camera, microphone and geolocation with empty allowlists; verify all three restrictions. +- framing (REASONED; mdn): X-Frame-Options: DENY or enforcing CSP frame-ancestors 'none' denies framing; supported frame-ancestors supersedes X-Frame-Options, and default-src does not cover it. frame-ancestors * overrides DENY. +- all-responses (REASONED; mdn, nginx-source): Send headers on every response, including errors; nginx add_header inherits parent headers only when the child has no header list, and always covers statuses outside its default set. This guide assesses setting headers only on 200 / as a common proxy misconfiguration. +- shared-cache (REASONED; mdn): Use Cache-Control: private for authenticated responses or no-store for sensitive ones; cache only public content unless authenticated caching is explicitly configured. +- cache-key (REASONED; mdn): A URL-only shared cache can leak one user's response to another; explicitly vary authenticated cache entries on the cookie or authorization header. +- browser-cache (REASONED; mdn): private still permits browser storage; no-store forbids private and shared storage and belongs on authenticated responses and responses setting session cookies. +- history-cache (REASONED; mdn): no-cache does not prevent a Back navigation from restoring a back/forward-cache snapshot without revalidation. +- logout-cache (REASONED; mdn): Send Clear-Site-Data on logout confirmation; cache clears browser cache and may clear back/forward cache depending on the browser. +- logout-cookies (REASONED; mdn): Clear-Site-Data cookies clears cookies across the registered domain and all subdomains, plus HTTP authentication credentials; invalidate the server session separately. +- logout-storage (REASONED; mdn): Clear-Site-Data storage removes origin local/session storage, IndexedDB and service-worker registrations; understand effects on companion applications before clearing. +- logout-header (REASONED; mdn): The cited MDN logout example includes cache, cookies, storage, executionContexts, prefetchCache and prerenderCache. +- verify-fetch (REASONED; mdn, curl): Fetch the final page with redirects restricted to HTTPS, bypass client proxies, discard the body and inspect the last response's headers and final status/URL; curl 8.23.0 documents the command options and write-out fields, while `-D -` printing each response's header block, and selecting the last one, are this guide's assessment. +- verify-values (REASONED; mdn): Judge header values on the final response, not presence on an earlier hop; missing final headers are findings and a failed fetch is inconclusive. +- verify-scanner (REASONED; scanner): Use the external Security Headers scanner for a second opinion; the guide states that it fetches with GET. +- verify-csp (REASONED; mdn): Unexpected console violations must be absent in normal use, but this alone is insufficient; confirm a deliberately disallowed resource is blocked while an allowed one loads. +- verify-cache (REASONED; mdn): Warm an authenticated URL in the shared cache, then request it as user A, user B and anonymously; each response must belong only to its caller. +- verify-logout (REASONED; mdn): After logout, Back must not restore authenticated content; verify a request to the login screen and inspect no-store and Clear-Site-Data separately in every supported browser. + +````json +{ "schema": 1, "checked": "2026-10-17", "documentation_checked": "2026-09", @@ -48,13 +220,6 @@ version_basis: { "sba79d4023eca": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L26" } }, - "scanner": { - "name": "Security Headers scanner", - "basis": "unknown", - "sources": { - "s7a94d0fcb6ad": "https://securityheaders.com/" - } - }, "helmet": { "name": "Helmet", "basis": "v8.1.0", @@ -69,6 +234,13 @@ version_basis: { "se79954c08919": "https://docs.djangoproject.com/en/5.2/ref/middleware/#module-django.middleware.security" } }, + "scanner": { + "name": "Security Headers scanner", + "basis": "unknown", + "sources": { + "s7a94d0fcb6ad": "https://securityheaders.com/" + } + }, "curl": { "name": "curl", "basis": "8.23.0", @@ -124,151 +296,5 @@ version_basis: { "verify-logout": {"text": "After logout, Back must not restore authenticated content; verify a request to the login screen and inspect no-store and Clear-Site-Data separately in every supported browser.", "components": ["mdn"], "sources": ["mdn:s9166d803cf3a", "mdn:s7f290c45ad37", "mdn:s46957d9dc1d5"], "status": "REASONED"} } } ---- -# Security headers for your application - - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-17; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- mdn: MDN HTTP documentation (unknown) -- nginx-source: nginx source (release-1.26.3) -- apache-source: Apache HTTP Server (2.4.69) -- caddy-source: Caddy source (v2.11.4) -- cloudflare-pages: Cloudflare Pages documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- scanner: Security Headers scanner (unknown) -- helmet: Helmet (v8.1.0) -- django: Django (5.2) -- curl: curl (8.23.0) - -Claims (full source mapping in the front matter): - -- placement (REASONED; mdn, helmet, django, nginx-source, apache-source, caddy-source, cloudflare-pages): Set response headers at a proxy with nginx add_header, Apache Header or Caddy header, in application middleware, or in a Cloudflare Pages _headers file for static asset responses; Pages Functions responses will likely need headers set in Functions code. Applying the three proxy directives to all six listed headers is this guide's inference. -- hsts (REASONED; mdn): Use HSTS max-age=31536000 with includeSubDomains only after HTTPS works across the domain; every subdomain is committed to HTTPS and max-age=0 disables HSTS. -- hsts-preload (REASONED; mdn): Omit preload unless its commitment is understood; removal is slow, takes weeks and depends on browser list updates. -- csp (REASONED; mdn): Start CSP at default-src 'self', tailor sources to the app and prefer script nonces or hashes over unsafe-inline; Verify requires an enforcing policy. -- csp-report-only (REASONED; mdn): Roll out Content-Security-Policy-Report-Only first on existing apps to find breakage; it enforces nothing and is not a substitute for enforcing CSP. -- nosniff (REASONED; mdn): Send X-Content-Type-Options: nosniff and verify that value on the final response. -- referrer (REASONED; mdn): Send Referrer-Policy: strict-origin-when-cross-origin and verify that final-response value. -- permissions (REASONED; mdn): Permissions-Policy disables camera, microphone and geolocation with empty allowlists; verify all three restrictions. -- framing (REASONED; mdn): X-Frame-Options: DENY or enforcing CSP frame-ancestors 'none' denies framing; supported frame-ancestors supersedes X-Frame-Options, and default-src does not cover it. frame-ancestors * overrides DENY. -- all-responses (REASONED; mdn, nginx-source): Send headers on every response, including errors; nginx add_header inherits parent headers only when the child has no header list, and always covers statuses outside its default set. This guide assesses setting headers only on 200 / as a common proxy misconfiguration. -- shared-cache (REASONED; mdn): Use Cache-Control: private for authenticated responses or no-store for sensitive ones; cache only public content unless authenticated caching is explicitly configured. -- cache-key (REASONED; mdn): A URL-only shared cache can leak one user's response to another; explicitly vary authenticated cache entries on the cookie or authorization header. -- browser-cache (REASONED; mdn): private still permits browser storage; no-store forbids private and shared storage and belongs on authenticated responses and responses setting session cookies. -- history-cache (REASONED; mdn): no-cache does not prevent a Back navigation from restoring a back/forward-cache snapshot without revalidation. -- logout-cache (REASONED; mdn): Send Clear-Site-Data on logout confirmation; cache clears browser cache and may clear back/forward cache depending on the browser. -- logout-cookies (REASONED; mdn): Clear-Site-Data cookies clears cookies across the registered domain and all subdomains, plus HTTP authentication credentials; invalidate the server session separately. -- logout-storage (REASONED; mdn): Clear-Site-Data storage removes origin local/session storage, IndexedDB and service-worker registrations; understand effects on companion applications before clearing. -- logout-header (REASONED; mdn): The cited MDN logout example includes cache, cookies, storage, executionContexts, prefetchCache and prerenderCache. -- verify-fetch (REASONED; mdn, curl): Fetch the final page with redirects restricted to HTTPS, bypass client proxies, discard the body and inspect the last response's headers and final status/URL; curl 8.23.0 documents the command options and write-out fields, while `-D -` printing each response's header block, and selecting the last one, are this guide's assessment. -- verify-values (REASONED; mdn): Judge header values on the final response, not presence on an earlier hop; missing final headers are findings and a failed fetch is inconclusive. -- verify-scanner (REASONED; scanner): Use the external Security Headers scanner for a second opinion; the guide states that it fetches with GET. -- verify-csp (REASONED; mdn): Unexpected console violations must be absent in normal use, but this alone is insufficient; confirm a deliberately disallowed resource is blocked while an allowed one loads. -- verify-cache (REASONED; mdn): Warm an authenticated URL in the shared cache, then request it as user A, user B and anonymously; each response must belong only to its caller. -- verify-logout (REASONED; mdn): After logout, Back must not restore authenticated content; verify a request to the login screen and inspect no-store and Clear-Site-Data separately in every supported browser. +```` - -TLS protects the transport; these response headers protect the page. Set them at the proxy ([`add_header`](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L110-L116) in [nginx.md](nginx.md), [`Header`](https://github.com/apache/httpd/blob/2.4.69/docs/manual/mod/mod_headers.xml#L311-L328) in [apache.md](apache.md), [`header`](https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/headers/caddyfile.go#L27-L48) in Caddy), in app middleware (helmet for Express per [nodejs.md](nodejs.md), Django's security settings per [python.md](python.md)), or in a [`_headers` file](https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L14) for Cloudflare Pages static asset responses. Cloudflare Pages does not apply `_headers` to Pages Functions responses; set those headers in [Functions code](https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L26). Applying the three proxy directives to all six headers below is this guide's inference. - -## The set worth shipping - -``` -Strict-Transport-Security: max-age=31536000; includeSubDomains -Content-Security-Policy: default-src 'self' -X-Content-Type-Options: nosniff -Referrer-Policy: strict-origin-when-cross-origin -Permissions-Policy: camera=(), microphone=(), geolocation=() -X-Frame-Options: DENY -``` - -Notes that keep these correct rather than decorative: - -- **HSTS** only after HTTPS provably works everywhere on the domain; `includeSubDomains` commits every subdomain to HTTPS. Leave the `preload` token off unless you have read what preload-list inclusion means: removal is possible but slow (weeks, and browser-dependent as each browser ships an updated list), so treat inclusion as a long-lived commitment. -- **CSP** is the one that needs tailoring. Start from `default-src 'self'`, add the sources your app actually uses, and prefer nonces or hashes over `'unsafe-inline'` for scripts. Roll out with `Content-Security-Policy-Report-Only` first on an existing app so you see what would break before enforcing. -- **frame-ancestors** in CSP supersedes `X-Frame-Options` in browsers that support it. The modern equivalent of `X-Frame-Options: DENY` is `frame-ancestors 'none'` in the CSP; add it explicitly, because `default-src` does not cover framing. Sending both `X-Frame-Options` and `frame-ancestors` keeps older scanners content. -- Headers belong on every response, including error pages; this guide assesses setting them only on `200 /` as a common proxy misconfiguration. In nginx, `add_header` inherits the parent's headers only if a child has no header list ([merge](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L665-L690); [list creation](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L788-L796)), and [`always`](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L843-L849) covers statuses outside its default set ([status handling](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L216-L218); [nginx.md](nginx.md)). - -## Do not cache authenticated responses in a shared cache - -A CDN or shared proxy that keys its cache on the URL alone can serve one user's authenticated response to another. Set `Cache-Control: private` on authenticated responses, or `no-store` on the most sensitive ones, and mark cacheable only what is truly public. If a shared cache must hold authenticated content, configure it explicitly to vary on the cookie or the authorization header rather than relying on its default URL-only key. - -## Keep authenticated pages out of the browser's own cache - -`private` stops a shared cache from holding a response, but it still lets the user's own browser store it: MDN defines `private` as a response the browser's local cache may keep. So the Back button can redisplay an authenticated page after logout, and a chat transcript can sit in the history of a shared machine. Set `Cache-Control: no-store` on any response that carries authenticated content or sets a session cookie; MDN defines `no-store` as "any caches of any kind (private or shared) should not store this response", which covers the browser too. - -On logout, clear what the browser already holds. `Cache-Control: no-cache` does not cover this, because a history navigation such as the Back button can restore a back/forward-cache snapshot without revalidating. Send the `Clear-Site-Data` header on the logout-confirmation response instead: its `cache` directive clears the browser cache and, depending on the browser, the back/forward cache as well, and `cookies` clears the client-side session cookie (invalidate the server-side session separately, as part of logout). MDN's own logout example is `Clear-Site-Data: "cache", "cookies", "storage", "executionContexts", "prefetchCache", "prerenderCache"`. Know the scope before sending the full set: `"cookies"` clears cookies across the entire registered domain (all subdomains) and also clears HTTP authentication credentials, and `"storage"` removes origin local/session storage, IndexedDB, and service-worker registrations, so a shared parent domain or a companion app on a subdomain is affected too. - -## Verify - -REASONED: final-response header inspection follows the cited MDN header documentation; curl 8.23.0 documents the command options and write-out fields, while `-D -` printing each response's header block, and selecting the last one, are this guide's assessment. No deployment URL was supplied for live checks, and this guide records no run; the following block states the expected values and treats failed fetches as inconclusive. - -```bash -# -q (first) skips curlrc, -g disables URL globbing, and -sS hides progress but shows errors. -# Fetch the FINAL page and read its response headers by eye. -L follows redirects (--proto-redir -# '=https' blocks an https-to-http downgrade) so you inspect the page a browser lands on, not a 302; -# --noproxy so no client proxy rewrites the headers; the body is discarded. -D - prints EVERY -# response's headers (a 103 Early Hints and each redirect hop, then the final one), and -w prints where -# you ended up, so read the block under the LAST "HTTP/.. " line: -curl -q -g -sS -L --proto-redir '=https' --noproxy '*' -D - -o /dev/null -w 'final: %{http_code} %{url_effective}\n' https://example.com/ -# In that final response check VALUES, not just presence: Strict-Transport-Security with a large, -# non-zero max-age (max-age=0 disables HSTS); an ENFORCING Content-Security-Policy, not only -# Content-Security-Policy-Report-Only (which enforces nothing); X-Content-Type-Options nosniff; -# Referrer-Policy strict-origin-when-cross-origin; Permissions-Policy denying camera, microphone, and -# geolocation; and an effective deny-framing policy: CSP frame-ancestors 'none', or X-Frame-Options -# DENY with NO enforcing frame-ancestors overriding it (an enforcing frame-ancestors always wins over -# X-Frame-Options, so frame-ancestors * with X-Frame-Options: DENY still allows framing). A header -# missing from the final response, or present only on an earlier hop, is a finding; a failed fetch is -# inconclusive. -``` - -REASONED: preceding block; final-response header values, external scanning and CSP discrimination, per the guide's cited MDN HTTP headers reference and Security header scanner. No deployed application or controlled browser application session is available, and network access is unavailable. bash -n and ShellCheck 0.11.0 exited 0 without diagnostics; no HTTP response or browser outcome was observed. Exposed responses omit effective controls or allow the forbidden resource; fixed final responses carry the stated values and block the forbidden resource while permitting the allowed control. Then scan with https://securityheaders.com/ from outside for a second opinion (it fetches with GET). No UNEXPECTED CSP console violations during normal application use is necessary but not sufficient: a permissive CSP produces none, and a correct policy still logs a violation when it blocks something (the negative test below, an attack, or an unwanted load). Confirm the policy actually blocks by loading a page with a deliberately disallowed resource (an inline script the policy forbids, or an off-origin image) and checking it is refused while an allowed resource still loads. - -REASONED: shared-cache caller separation, per the guide's cited MDN Cache-Control. No deployed shared cache or authenticated A/B accounts are available; this prose supplies no local command or configuration to execute. No response sequence was observed. Exposed caching replays a previous caller's private content; fixed responses after warming the same URL belong only to their current authenticated or anonymous caller. For an authenticated route behind a shared cache, request the same URL as user A, then as user B, then anonymously, after warming the cache; each response must reflect only its own caller, never the one before it. - -REASONED: logout, browser history and response-header inspection, per the guide's cited MDN Cache-Control, MDN Clear-Site-Data and MDN back/forward cache. No application session or supported-browser deployment matrix is available; this prose supplies no local command or configuration to execute. No logout sequence was observed. Exposed history restores authenticated content after logout; the expected fixed outcome is a new request reaching login, with no-store and Clear-Site-Data checked independently in every supported browser. In a real browser, log in, open an authenticated page, then log out and press the Back button: the authenticated page must not reappear from history. The browser should re-request it and land on the login screen. That confirms the logout outcome, not which header produced it, so also inspect the authenticated and logout responses for the intended `no-store` and `Clear-Site-Data` headers. Because the back/forward-cache clearing is browser-dependent, run the check in each browser you support. - -## Sources (checked September 2026) - -- MDN HTTP headers reference (Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, X-Frame-Options): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers -- MDN Cache-Control (`no-store` forbids any cache including the browser; `private` still permits the browser's local cache): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control -- MDN Clear-Site-Data (the `cache` directive clears the browser cache and, depending on the browser, the back/forward cache; the logout example): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Clear-Site-Data -- MDN back/forward cache (a history navigation restores a snapshot without revalidating): https://developer.mozilla.org/en-US/docs/Glossary/bfcache -- Security header scanner: https://securityheaders.com/ -- Helmet v8.1.0 response-header middleware (checked October 2026): https://github.com/helmetjs/helmet/blob/v8.1.0/README.md#L3 -- Django 5.2 SecurityMiddleware (checked October 2026): https://docs.djangoproject.com/en/5.2/ref/middleware/#module-django.middleware.security -- nginx release-1.26.3 add_header directive (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L110-L116 -- nginx release-1.26.3 add_header list creation (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L788-L796 -- nginx release-1.26.3 header inheritance (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L665-L690 -- nginx release-1.26.3 always status handling (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L216-L218 -- nginx release-1.26.3 always option (checked October 2026): https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L843-L849 -- Apache 2.4.69 Header response-header directive (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/docs/manual/mod/mod_headers.xml#L311-L328 -- Caddy v2.11.4 header response-header directive (checked October 2026): https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/headers/caddyfile.go#L27-L48 -- Cloudflare Pages _headers on static assets (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L14 -- Cloudflare Pages _headers Pages Functions limit (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L26 -- curl 8.23.0 --disable (first argument skips curlrc) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/disable.md#L18-L20 -- curl 8.23.0 --globoff (disables URL globbing) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/globoff.md#L19-L22 -- curl 8.23.0 --silent (quiet mode with --show-error) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/silent.md#L20-L25 -- curl 8.23.0 --show-error (reports errors with --silent) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/show-error.md#L19 -- curl 8.23.0 --location (follows redirects) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/location.md#L23-L26 -- curl 8.23.0 --proto-redir (restricts redirect protocols) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/proto-redir.md#L19-L20 -- curl 8.23.0 --proto (= permits only the named protocol) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/proto.md#L30-L33 -- curl 8.23.0 --noproxy (* disables proxy use) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/noproxy.md#L18-L23 -- curl 8.23.0 --dump-header (- writes headers to stdout) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/dump-header.md#L21-L23 -- curl 8.23.0 --output (/dev/null discards the body) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/output.md#L53-L55 -- curl 8.23.0 --write-out (format and variables) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L20-L29 -- curl 8.23.0 --write-out http_code (last response code) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L112-L114 -- curl 8.23.0 --write-out url_effective (last fetched URL) (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L358-L360 -- curl 8.23.0 --disable short form -q (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/disable.md#L5 -- curl 8.23.0 --globoff short form -g (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/globoff.md#L5 -- curl 8.23.0 --silent short form -s (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/silent.md#L5 -- curl 8.23.0 --show-error short form -S (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/show-error.md#L5 -- curl 8.23.0 --location short form -L (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/location.md#L5 -- curl 8.23.0 --dump-header short form -D (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/dump-header.md#L5 -- curl 8.23.0 --output short form -o (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/output.md#L6 -- curl 8.23.0 --write-out short form -w (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L5 diff --git a/plugin/skills/secure-deployment-config/references/kubernetes.md b/plugin/skills/secure-deployment-config/references/kubernetes.md index 8306ecb13..d92220635 100644 --- a/plugin/skills/secure-deployment-config/references/kubernetes.md +++ b/plugin/skills/secure-deployment-config/references/kubernetes.md @@ -1,311 +1,16 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-27", - "documentation_checked": "2026-09", - "body_sha256": "1e80b5ac6d9df2701bae1929db0d6cb2f9ce6416a58415b189d509640b8a44c8", - "components": { - "kubernetes": { - "name": "Kubernetes documentation", - "basis": "unknown", - "sources": { - "s0f11d6ed39b7": "https://kubernetes.io/blog/2026/01/29/ingress-nginx-statement/", - "sca23283c5a9b": "https://kubernetes.io/docs/concepts/services-networking/gateway/", - "sd8c8df3b9ddc": "https://kubernetes.io/docs/concepts/services-networking/ingress/", - "se41a53ea6778": "https://kubernetes.io/docs/reference/config-api/kubeconfig.v1/", - "sae633285120e": "https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/", - "sd59fb2f35cd7": "https://kubernetes.io/docs/reference/networking/ports-and-protocols/", - "s01f081fed0ff": "https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/", - "s49a29258961b": "https://kubernetes.io/docs/reference/access-authn-authz/kubelet-authn-authz/", - "s04f0c3099fd7": "https://kubernetes.io/docs/reference/kubectl/jsonpath/" - } - }, - "kubelet": { - "name": "Kubernetes kubelet/kubeadm source", - "basis": "v1.37.1", - "sources": { - "sc6056d6413a0": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L196-L224", - "s227d2a259fd3": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L373-L398", - "s568cd52cc674": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/apis/config/v1beta1/defaults.go#L85-L102", - "s2daf96dabbdf": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L281", - "s24ba323c3dab": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L282", - "sd81692a1876f": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L33-L48", - "s0eb30acf2f46": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L95-L100", - "s1c86aaef7307": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L144-L188", - "sb98bd327db92": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/utils.go#L66-L71", - "s3fc405855b4a": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/staging/src/k8s.io/kubelet/config/v1beta1/types.go#L160-L176", - "s98cf98130201": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L181-L250", - "s8bea69523ed9": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L339-L375", - "sa02a895a475c": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/cluster/ports/ports.go#L32-L37", - "s6b753b247f8a": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L477-L572", - "sd00d4cdb9276": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/stats/handler.go#L109-L133", - "s02441dcae3cf": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L577-L610" - } - }, - "envoy": { - "name": "Envoy Gateway", - "basis": "v1.9.1", - "sources": { - "s6412abbdfdbd": "https://github.com/envoyproxy/gateway/releases/download/v1.9.1/quickstart.yaml" - } - }, - "gateway": { - "name": "Gateway API documentation", - "basis": "unknown", - "sources": { - "se0e45c61dc65": "https://gateway-api.sigs.k8s.io/guides/getting-started/introduction/", - "se1f28d98d168": "https://gateway-api.sigs.k8s.io/guides/user-guides/tls/", - "s4a971425e1d1": "https://gateway-api.sigs.k8s.io/guides/user-guides/http-routing/", - "s8d02fbc742ba": "https://gateway-api.sigs.k8s.io/guides/user-guides/http-redirect-rewrite/" - } - }, - "cert-manager": { - "name": "cert-manager Gateway support minimum", - "basis": "1.15", - "sources": { - "s72bdb97928a8": "https://cert-manager.io/docs/usage/gateway/", - "s8d82244c2d6d": "https://cert-manager.io/docs/configuration/acme/http01/" - } - }, - "apache": { - "name": "Apache htpasswd documentation", - "basis": "unknown", - "sources": { - "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" - } - }, - "authelia": { - "name": "Authelia documentation", - "basis": "unknown", - "sources": { - "s0cb4f49871b5": "https://www.authelia.com/integration/proxies/introduction/", - "scfa9e2cc2ef5": "https://www.authelia.com/integration/kubernetes/envoy/gateway/" - } - }, - "eks": { - "name": "Amazon EKS documentation", - "basis": "unknown", - "sources": { - "s9724ce882930": "https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html" - } - }, - "eks-ami": { - "name": "Amazon EKS AMI bootstrap", - "basis": "6caf8311a3c6a8da71ac7e5e83f9c2e06287039a", - "sources": { - "sbe1fce93c7b2": "https://github.com/awslabs/amazon-eks-ami/blob/6caf8311a3c6a8da71ac7e5e83f9c2e06287039a/nodeadm/internal/kubelet/config.go" - } - }, - "gke": { - "name": "GKE documentation", - "basis": "unknown", - "sources": { - "s0f44d89575ed": "https://docs.cloud.google.com/kubernetes-engine/docs/concepts/network-isolation#how_authorized_networks_work" - } - }, - "gke-min": { - "name": "GKE new-cluster read-only-port boundary", - "basis": "1.32", - "sources": { - "s4c7be4adcab4": "https://docs.cloud.google.com/kubernetes-engine/docs/how-to/disable-kubelet-readonly-port" - } - }, - "aks": { - "name": "AKS documentation", - "basis": "unknown", - "sources": { - "s897dd27ab9a1": "https://learn.microsoft.com/en-us/azure/aks/api-server-authorized-ip-ranges" - } - }, - "curl": { - "name": "curl documentation", - "basis": "unknown", - "sources": { - "s2b2686afaf41": "https://curl.se/docs/manpage.html", - "s7d4a6e627b1b": "https://curl.se/libcurl/c/libcurl-errors.html" - } - }, - "nmap": { - "name": "Nmap documentation", - "basis": "unknown", - "sources": { - "sb7d0e7eb5141": "https://nmap.org/book/man-host-discovery.html", - "s1818abeb3f92": "https://nmap.org/book/man-port-specification.html", - "s79c95712dd8d": "https://nmap.org/book/man-misc-options.html" - } - }, - "traefik": { - "name": "Traefik documentation", - "basis": "unknown", - "sources": { - "s06f2ee45331e": "https://doc.traefik.io/traefik/reference/install-configuration/providers/kubernetes/kubernetes-gateway/" - } - }, - "cilium": { - "name": "Cilium documentation", - "basis": "unknown", - "sources": { - "sacaa7a78461d": "https://docs.cilium.io/en/stable/network/servicemesh/gateway-api/gateway-api/" - } - }, - "envoy-rolling": { - "name": "Envoy Gateway documentation (rolling)", - "basis": "unknown", - "sources": { - "se833317feea7": "https://gateway.envoyproxy.io/docs/tasks/security/secure-gateways/", - "sb5fa4f4ffa1a": "https://gateway.envoyproxy.io/docs/tasks/security/basic-auth/", - "s7fdd76c5d549": "https://gateway.envoyproxy.io/docs/tasks/security/oidc/", - "s100049c74adf": "https://gateway.envoyproxy.io/docs/tasks/security/ext-auth/", - "s9b1d646cb718": "https://gateway.envoyproxy.io/docs/tasks/traffic/http-redirect/", - "s881f08174c54": "https://gateway.envoyproxy.io/v1.9/install/install-helm/", - "s9ad0f418b20b": "https://gateway.envoyproxy.io/v1.9/tasks/quickstart/" - } - } - }, - "claims": { - "ingress-migration": {"text": "ingress-nginx retired in March 2026 with no subsequent updates; detect its labeled pods and migrate. Gateway API is recommended and Ingress API is frozen.", "components": ["kubernetes"], "sources": ["kubernetes:s0f11d6ed39b7", "kubernetes:sca23283c5a9b", "kubernetes:sd8c8df3b9ddc"], "status": "REASONED"}, - "gateway-install": {"text": "Install Envoy Gateway chart v1.9.1 and its default Gateway CRDs; separately create GatewayClass eg with the documented controllerName and confirm Accepted=True.", "components": ["envoy", "gateway", "envoy-rolling"], "sources": ["envoy-rolling:s881f08174c54", "envoy:s6412abbdfdbd", "gateway:se0e45c61dc65"], "status": "REASONED"}, - "gateway-alternatives": {"text": "Traefik uses providers.kubernetesGateway; Cilium uses gatewayAPI.enabled=true and requires kube-proxy replacement.", "components": ["traefik", "cilium"], "sources": ["traefik:s06f2ee45331e", "cilium:sacaa7a78461d"], "status": "REASONED"}, - "gateway-tls": {"text": "Gateway HTTPS listener 443 terminates TLS using app-tls for app.example.com; HTTP 80 is for redirect and ACME challenges.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:se1f28d98d168", "envoy-rolling:se833317feea7"], "status": "REASONED"}, - "gateway-route": {"text": "HTTPRoute app attaches only to the HTTPS listener via sectionName and forwards to the ClusterIP app Service on port 80.", "components": ["gateway"], "sources": ["gateway:s4a971425e1d1"], "status": "REASONED"}, - "gateway-redirect": {"text": "The HTTP route uses RequestRedirect with scheme https and statusCode 301.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:s8d02fbc742ba", "envoy-rolling:s9b1d646cb718"], "status": "REASONED"}, - "cert-manager-enable": {"text": "From cert-manager 1.15, enable config.gatewayAPI.enabled; install Gateway CRDs before startup or restart cert-manager afterwards.", "components": ["cert-manager"], "sources": ["cert-manager:s72bdb97928a8"], "status": "REASONED"}, - "cert-manager-issue": {"text": "Gateway issuer annotations produce Certificates per HTTPS-listener Secret using listener hostnames; ACME gatewayHTTPRoute parentRefs selects the challenge Gateway.", "components": ["cert-manager"], "sources": ["cert-manager:s72bdb97928a8", "cert-manager:s8d82244c2d6d"], "status": "REASONED"}, - "basic-auth-policy": {"text": "Gateway API has no standard auth filter; Envoy SecurityPolicy attaches Basic auth to Gateway/HTTPRoute/GRPCRoute using an htpasswd Secret.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:s4a971425e1d1", "envoy-rolling:sb5fa4f4ffa1a"], "status": "REASONED"}, - "basic-auth-hash": {"text": "Envoy supports SHA hashes only; htpasswd -s is weak SHA-1, so use long random passwords over TLS and keep application login.", "components": ["apache", "envoy-rolling"], "sources": ["envoy-rolling:sb5fa4f4ffa1a", "apache:scde9004bb967"], "status": "REASONED"}, - "basic-auth-input": {"text": "htpasswd -i reads stdin, unlike argv-exposing -b; confirm twice, create with -cis, verify with -vi and only then create the Kubernetes Secret.", "components": ["apache", "envoy-rolling"], "sources": ["apache:scde9004bb967", "envoy-rolling:sb5fa4f4ffa1a"], "status": "REASONED"}, - "oidc": {"text": "Envoy SecurityPolicy oidc uses issuer, clientID, clientSecret and redirectURL; enforce MFA at the identity provider.", "components": ["envoy-rolling"], "sources": ["envoy-rolling:s7fdd76c5d549"], "status": "REASONED"}, - "external-auth": {"text": "Authelia is an authorization endpoint, not a traffic proxy; Envoy extAuth.http targets its Service and /api/authz/ext-authz/.", "components": ["authelia", "envoy-rolling"], "sources": ["authelia:s0cb4f49871b5", "authelia:scfa9e2cc2ef5", "envoy-rolling:s100049c74adf"], "status": "REASONED"}, - "workload-exposure": {"text": "Expose workloads only through the Gateway LoadBalancer; keep databases on ClusterIP without routes and enforce pod access with NetworkPolicies plus database TLS/auth.", "components": ["envoy-rolling", "kubernetes"], "sources": ["envoy-rolling:s9ad0f418b20b", "kubernetes:sca23283c5a9b"], "status": "REASONED"}, - "secrets": {"text": "Store credentials in Secrets or an external operator, not committed ConfigMaps or environment literals; kubeconfigs containing credentials also need secret handling.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778", "kubernetes:sae633285120e"], "status": "REASONED"}, - "eks-endpoint": {"text": "EKS API defaults public; restrict public CIDRs or use private access. Private-only endpoints can resolve publicly to private VPC addresses.", "components": ["eks"], "sources": ["eks:s9724ce882930"], "status": "REASONED"}, - "gke-endpoints": {"text": "GKE authorized networks constrain IP-based access, not its separately IAM-gated DNS endpoint; inspect both and disable unused DNS access.", "components": ["gke"], "sources": ["gke:s0f44d89575ed"], "status": "REASONED"}, - "aks-endpoint": {"text": "Restrict AKS API access with authorized IP ranges; workload Gateway protection does not secure the control plane.", "components": ["aks"], "sources": ["aks:s897dd27ab9a1"], "status": "REASONED"}, - "kubeconfig": {"text": "Embedded tokens or client keys confer their RBAC access without another factor; exec-plugin configs may be only pointers, but args/env can themselves hold credentials.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778"], "status": "REASONED"}, - "api-port": {"text": "API server commonly uses 6443, but providers may use 443; use the complete kubeconfig endpoint and its actual port.", "components": ["kubernetes", "eks"], "sources": ["kubernetes:sd59fb2f35cd7", "eks:s9724ce882930"], "status": "REASONED"}, - "etcd-ports": {"text": "etcd client/peer defaults are 2379/2380; require certificates, restrict holders and private access, and retain loopback listeners used by the local API server.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7", "kubernetes:s01f081fed0ff"], "status": "REASONED"}, - "etcd-storage": {"text": "API resources default to plaintext storage in etcd; valid client certificates are the boundary and etcd access is equivalent to cluster root.", "components": ["kubernetes"], "sources": ["kubernetes:sae633285120e", "kubernetes:s01f081fed0ff"], "status": "REASONED"}, - "scheduler-port": {"text": "Scheduler default 10259 must remain restricted to cluster clients.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, - "controller-port": {"text": "Controller-manager default 10257 must remain restricted to cluster clients.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, - "proxy-port": {"text": "Worker kube-proxy health default 10256 needs separate node exposure checks.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, - "nodeports": {"text": "NodePort defaults span 30000 to 32767 TCP/UDP; Service listings do not actively probe that range.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, - "kubelet-legacy-anonymous": {"text": "Legacy kubelet flags default anonymous-auth=true; explicitly disable it where flags control authentication.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0", "kubelet:s227d2a259fd3"], "status": "REASONED"}, - "kubelet-legacy-webhook": {"text": "Legacy authentication-token-webhook defaults false; explicitly enable it where flags configure authentication.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0"], "status": "REASONED"}, - "kubelet-legacy-authz": {"text": "Legacy authorization-mode defaults AlwaysAllow; set Webhook where flags configure authorization.", "components": ["kubelet", "kubernetes"], "sources": ["kubelet:sc6056d6413a0", "kubernetes:s49a29258961b"], "status": "REASONED"}, - "kubelet-file-anonymous": {"text": "v1beta1 KubeletConfiguration instead defaults authentication.anonymous.enabled=false.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, - "kubelet-file-webhook": {"text": "v1beta1 KubeletConfiguration defaults authentication.webhook.enabled=true.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, - "kubelet-file-authz": {"text": "v1beta1 KubeletConfiguration defaults authorization.mode=Webhook.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, - "kubelet-precedence": {"text": "Explicit flags override --config; --config-dir drop-ins override defaults and the file. Inspect all effective settings together.", "components": ["kubelet"], "sources": ["kubelet:s2daf96dabbdf", "kubelet:s24ba323c3dab"], "status": "REASONED"}, - "kubeadm": {"text": "kubeadm sets anonymous false, token webhook true, Webhook authorization and readOnlyPort 0; user overrides are preserved with warnings.", "components": ["kubelet"], "sources": ["kubelet:sd81692a1876f", "kubelet:s0eb30acf2f46", "kubelet:s1c86aaef7307", "kubelet:sb98bd327db92"], "status": "REASONED"}, - "eks-bootstrap": {"text": "The pinned EKS AMI bootstrap writes Anonymous.Enabled=false, Mode=Webhook and ReadOnlyPort=0; do not generalize to all managed nodes.", "components": ["eks-ami"], "sources": ["eks-ami:sbe1fce93c7b2"], "status": "REASONED"}, - "kubelet-bind": {"text": "Secured kubelet defaults to address 0.0.0.0 on 10250; wildcard help includes both families. Bind private addresses and restrict authorized callers.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674", "kubelet:s3fc405855b4a", "kubelet:s227d2a259fd3", "kubelet:s98cf98130201"], "status": "REASONED"}, - "kubelet-exec": {"text": "10250 debugging handlers can execute in containers; authentication does not justify public reachability.", "components": ["kubelet"], "sources": ["kubelet:s8bea69523ed9", "kubelet:s3fc405855b4a", "kubelet:s98cf98130201", "kubelet:s02441dcae3cf"], "status": "REASONED"}, - "readonly-default": {"text": "Legacy read-only port defaults 10255, while v1beta1 readOnlyPort defaults 0; explicitly disable it and remove enabling overrides.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0", "kubelet:sa02a895a475c", "kubelet:s3fc405855b4a"], "status": "REASONED"}, - "readonly-auth": {"text": "Read-only server uses plain HTTP with no authentication or authorization filter; its caller-supplied bind wiring was outside the inspected source subset.", "components": ["kubelet"], "sources": ["kubelet:s98cf98130201", "kubelet:s8bea69523ed9"], "status": "REASONED"}, - "readonly-paths": {"text": "Read-only handlers expose /pods, /stats/summary, metrics variants and health; v1.37.1 has no /spec handler. Securing 10250 does not secure 10255.", "components": ["kubelet"], "sources": ["kubelet:s6b753b247f8a", "kubelet:sd00d4cdb9276"], "status": "REASONED"}, - "gke-readonly": {"text": "GKE disables the read-only port by default only for new clusters running 1.32+; inspect effective configuration on older or upgraded clusters.", "components": ["gke-min"], "sources": ["gke-min:s4c7be4adcab4"], "status": "REASONED"}, - "verify-services": {"text": "Only the Gateway should have NodePort/LoadBalancer exposure; verify its public address/DNS and Certificate Ready=True.", "components": ["kubernetes", "envoy-rolling", "cert-manager"], "sources": ["kubernetes:s04f0c3099fd7", "envoy-rolling:s9ad0f418b20b", "cert-manager:s72bdb97928a8"], "status": "REASONED", "verify": [1]}, - "verify-redirect": {"text": "HTTP should return 301 to the HTTPS application URL.", "components": ["gateway"], "sources": ["gateway:s8d02fbc742ba"], "status": "REASONED", "verify": [1]}, - "verify-basic": {"text": "Anonymous Basic-protected access should return 401; require SecurityPolicy Accepted=True and a valid credential reaching the app without the gateway Basic challenge.", "components": ["curl", "envoy-rolling"], "sources": ["envoy-rolling:sb5fa4f4ffa1a", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, - "verify-api-target": {"text": "Use the whole kubeconfig endpoint, preserving IPv6 and inferring a schemeless endpoint from both cluster and user TLS settings, not a hardcoded 6443.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778", "kubernetes:sd59fb2f35cd7"], "status": "REASONED", "verify": [1]}, - "verify-proxies": {"text": "Disable curl config and configured proxies for direct probes; a transparent TLS middlebox can still answer for an unreachable target.", "components": ["curl"], "sources": ["curl:s2b2686afaf41", "curl:s7d4a6e627b1b"], "status": "REASONED", "verify": [1]}, - "verify-api-tcp": {"text": "Outside allowed ranges, nc success proves something accepted TCP; corroborate failed probes with provider ranges and multiple vantage points.", "components": ["kubernetes", "eks"], "sources": ["kubernetes:sd59fb2f35cd7", "eks:s9724ce882930"], "status": "REASONED", "verify": [1]}, - "verify-api-http": {"text": "Any HTTP status with curl exit 0 means answered; exit 60 means a TLS peer answered, 7 failed connect, 28 is ambiguous timeout and 6 is DNS failure, not privacy proof.", "components": ["curl", "eks"], "sources": ["curl:s7d4a6e627b1b", "eks:s9724ce882930"], "status": "REASONED", "verify": [1]}, - "verify-node-scan": {"text": "Scan control-plane hosts first and every public node address in both families; workers alone miss API/etcd and separate etcd hosts are absent from kubectl nodes.", "components": ["kubernetes", "nmap"], "sources": ["kubernetes:sd59fb2f35cd7", "kubernetes:s01f081fed0ff", "nmap:sb7d0e7eb5141", "nmap:s1818abeb3f92", "nmap:s79c95712dd8d"], "status": "REASONED", "verify": [1]}, - "verify-kubelet": {"text": "Untrusted clients should reach neither kubelet port after isolation, while authorized clients still reach 10250; readOnlyPort=0 should leave no 10255 listener even for authorized clients.", "components": ["kubelet"], "sources": ["kubelet:s98cf98130201", "kubelet:s3fc405855b4a"], "status": "REASONED", "verify": [1]} - } -} ---- # Kubernetes: Gateway API TLS and authentication - -**Version basis** +Written against Kubernetes kubelet/kubeadm source v1.37.1, Envoy Gateway v1.9.1, cert-manager Gateway support minimum 1.15, and GKE new-cluster read-only-port boundary 1.32; 1 further source is pinned to a commit and 12 carry no recorded version. Metadata reviewed 2026-09-27; documentation checked September 2026. The guide records 50 claims, all reasoned from cited sources. -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. -Metadata reviewed 2026-09-27; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +The cluster equivalents of this repository's rules: nothing reaches a workload except through the TLS-terminating entry point (a Gateway API `Gateway`), and no Service becomes public through a casual `type: LoadBalancer` or `NodePort`; the entry point's own Service is the only exception. -Components (recorded basis): +**If you run ingress-nginx today, migrate.** Earlier versions of this guide built on ingress-nginx. The Kubernetes project retired it in March 2026: per the Kubernetes Steering and Security Response Committees, "there will be no more releases for bug fixes, security patches, or any updates of any kind after the project is retired", and "choosing to remain with Ingress NGINX after its retirement leaves you and your users vulnerable to attack" (as of September 2026; statement linked in Sources). Detect it with cluster-admin permissions: `kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx`. Any pod returned means migration is required; the `nginx.ingress.kubernetes.io/*` annotations die with the controller. Kubernetes recommends Gateway API over Ingress (whose API is now frozen) and links a migration guide from its Gateway API page. The rest of this guide is the Gateway API form of the old rules. -- kubernetes: Kubernetes documentation (unknown) -- kubelet: Kubernetes kubelet/kubeadm source (v1.37.1) -- envoy: Envoy Gateway (v1.9.1) -- gateway: Gateway API documentation (unknown) -- cert-manager: cert-manager Gateway support minimum (1.15) -- apache: Apache htpasswd documentation (unknown) -- authelia: Authelia documentation (unknown) -- eks: Amazon EKS documentation (unknown) -- eks-ami: Amazon EKS AMI bootstrap (6caf8311a3c6a8da71ac7e5e83f9c2e06287039a) -- gke: GKE documentation (unknown) -- gke-min: GKE new-cluster read-only-port boundary (1.32) -- aks: AKS documentation (unknown) -- curl: curl documentation (unknown) -- nmap: Nmap documentation (unknown) -- traefik: Traefik documentation (unknown) -- cilium: Cilium documentation (unknown) -- envoy-rolling: Envoy Gateway documentation (rolling) (unknown) +## 1. Gateway API with a maintained implementation -Claims (full source mapping in the front matter): - -- ingress-migration (REASONED; kubernetes): ingress-nginx retired in March 2026 with no subsequent updates; detect its labeled pods and migrate. Gateway API is recommended and Ingress API is frozen. -- gateway-install (REASONED; envoy, gateway, envoy-rolling): Install Envoy Gateway chart v1.9.1 and its default Gateway CRDs; separately create GatewayClass eg with the documented controllerName and confirm Accepted=True. -- gateway-alternatives (REASONED; traefik, cilium): Traefik uses providers.kubernetesGateway; Cilium uses gatewayAPI.enabled=true and requires kube-proxy replacement. -- gateway-tls (REASONED; gateway, envoy-rolling): Gateway HTTPS listener 443 terminates TLS using app-tls for app.example.com; HTTP 80 is for redirect and ACME challenges. -- gateway-route (REASONED; gateway): HTTPRoute app attaches only to the HTTPS listener via sectionName and forwards to the ClusterIP app Service on port 80. -- gateway-redirect (REASONED; gateway, envoy-rolling): The HTTP route uses RequestRedirect with scheme https and statusCode 301. -- cert-manager-enable (REASONED; cert-manager): From cert-manager 1.15, enable config.gatewayAPI.enabled; install Gateway CRDs before startup or restart cert-manager afterwards. -- cert-manager-issue (REASONED; cert-manager): Gateway issuer annotations produce Certificates per HTTPS-listener Secret using listener hostnames; ACME gatewayHTTPRoute parentRefs selects the challenge Gateway. -- basic-auth-policy (REASONED; gateway, envoy-rolling): Gateway API has no standard auth filter; Envoy SecurityPolicy attaches Basic auth to Gateway/HTTPRoute/GRPCRoute using an htpasswd Secret. -- basic-auth-hash (REASONED; apache, envoy-rolling): Envoy supports SHA hashes only; htpasswd -s is weak SHA-1, so use long random passwords over TLS and keep application login. -- basic-auth-input (REASONED; apache, envoy-rolling): htpasswd -i reads stdin, unlike argv-exposing -b; confirm twice, create with -cis, verify with -vi and only then create the Kubernetes Secret. -- oidc (REASONED; envoy-rolling): Envoy SecurityPolicy oidc uses issuer, clientID, clientSecret and redirectURL; enforce MFA at the identity provider. -- external-auth (REASONED; authelia, envoy-rolling): Authelia is an authorization endpoint, not a traffic proxy; Envoy extAuth.http targets its Service and /api/authz/ext-authz/. -- workload-exposure (REASONED; envoy-rolling, kubernetes): Expose workloads only through the Gateway LoadBalancer; keep databases on ClusterIP without routes and enforce pod access with NetworkPolicies plus database TLS/auth. -- secrets (REASONED; kubernetes): Store credentials in Secrets or an external operator, not committed ConfigMaps or environment literals; kubeconfigs containing credentials also need secret handling. -- eks-endpoint (REASONED; eks): EKS API defaults public; restrict public CIDRs or use private access. Private-only endpoints can resolve publicly to private VPC addresses. -- gke-endpoints (REASONED; gke): GKE authorized networks constrain IP-based access, not its separately IAM-gated DNS endpoint; inspect both and disable unused DNS access. -- aks-endpoint (REASONED; aks): Restrict AKS API access with authorized IP ranges; workload Gateway protection does not secure the control plane. -- kubeconfig (REASONED; kubernetes): Embedded tokens or client keys confer their RBAC access without another factor; exec-plugin configs may be only pointers, but args/env can themselves hold credentials. -- api-port (REASONED; kubernetes, eks): API server commonly uses 6443, but providers may use 443; use the complete kubeconfig endpoint and its actual port. -- etcd-ports (REASONED; kubernetes): etcd client/peer defaults are 2379/2380; require certificates, restrict holders and private access, and retain loopback listeners used by the local API server. -- etcd-storage (REASONED; kubernetes): API resources default to plaintext storage in etcd; valid client certificates are the boundary and etcd access is equivalent to cluster root. -- scheduler-port (REASONED; kubernetes): Scheduler default 10259 must remain restricted to cluster clients. -- controller-port (REASONED; kubernetes): Controller-manager default 10257 must remain restricted to cluster clients. -- proxy-port (REASONED; kubernetes): Worker kube-proxy health default 10256 needs separate node exposure checks. -- nodeports (REASONED; kubernetes): NodePort defaults span 30000 to 32767 TCP/UDP; Service listings do not actively probe that range. -- kubelet-legacy-anonymous (REASONED; kubelet): Legacy kubelet flags default anonymous-auth=true; explicitly disable it where flags control authentication. -- kubelet-legacy-webhook (REASONED; kubelet): Legacy authentication-token-webhook defaults false; explicitly enable it where flags configure authentication. -- kubelet-legacy-authz (REASONED; kubelet, kubernetes): Legacy authorization-mode defaults AlwaysAllow; set Webhook where flags configure authorization. -- kubelet-file-anonymous (REASONED; kubelet): v1beta1 KubeletConfiguration instead defaults authentication.anonymous.enabled=false. -- kubelet-file-webhook (REASONED; kubelet): v1beta1 KubeletConfiguration defaults authentication.webhook.enabled=true. -- kubelet-file-authz (REASONED; kubelet): v1beta1 KubeletConfiguration defaults authorization.mode=Webhook. -- kubelet-precedence (REASONED; kubelet): Explicit flags override --config; --config-dir drop-ins override defaults and the file. Inspect all effective settings together. -- kubeadm (REASONED; kubelet): kubeadm sets anonymous false, token webhook true, Webhook authorization and readOnlyPort 0; user overrides are preserved with warnings. -- eks-bootstrap (REASONED; eks-ami): The pinned EKS AMI bootstrap writes Anonymous.Enabled=false, Mode=Webhook and ReadOnlyPort=0; do not generalize to all managed nodes. -- kubelet-bind (REASONED; kubelet): Secured kubelet defaults to address 0.0.0.0 on 10250; wildcard help includes both families. Bind private addresses and restrict authorized callers. -- kubelet-exec (REASONED; kubelet): 10250 debugging handlers can execute in containers; authentication does not justify public reachability. -- readonly-default (REASONED; kubelet): Legacy read-only port defaults 10255, while v1beta1 readOnlyPort defaults 0; explicitly disable it and remove enabling overrides. -- readonly-auth (REASONED; kubelet): Read-only server uses plain HTTP with no authentication or authorization filter; its caller-supplied bind wiring was outside the inspected source subset. -- readonly-paths (REASONED; kubelet): Read-only handlers expose /pods, /stats/summary, metrics variants and health; v1.37.1 has no /spec handler. Securing 10250 does not secure 10255. -- gke-readonly (REASONED; gke-min): GKE disables the read-only port by default only for new clusters running 1.32+; inspect effective configuration on older or upgraded clusters. -- verify-services (REASONED; kubernetes, envoy-rolling, cert-manager): Only the Gateway should have NodePort/LoadBalancer exposure; verify its public address/DNS and Certificate Ready=True. -- verify-redirect (REASONED; gateway): HTTP should return 301 to the HTTPS application URL. -- verify-basic (REASONED; curl, envoy-rolling): Anonymous Basic-protected access should return 401; require SecurityPolicy Accepted=True and a valid credential reaching the app without the gateway Basic challenge. -- verify-api-target (REASONED; kubernetes): Use the whole kubeconfig endpoint, preserving IPv6 and inferring a schemeless endpoint from both cluster and user TLS settings, not a hardcoded 6443. -- verify-proxies (REASONED; curl): Disable curl config and configured proxies for direct probes; a transparent TLS middlebox can still answer for an unreachable target. -- verify-api-tcp (REASONED; kubernetes, eks): Outside allowed ranges, nc success proves something accepted TCP; corroborate failed probes with provider ranges and multiple vantage points. -- verify-api-http (REASONED; curl, eks): Any HTTP status with curl exit 0 means answered; exit 60 means a TLS peer answered, 7 failed connect, 28 is ambiguous timeout and 6 is DNS failure, not privacy proof. -- verify-node-scan (REASONED; kubernetes, nmap): Scan control-plane hosts first and every public node address in both families; workers alone miss API/etcd and separate etcd hosts are absent from kubectl nodes. -- verify-kubelet (REASONED; kubelet): Untrusted clients should reach neither kubelet port after isolation, while authorized clients still reach 10250; readOnlyPort=0 should leave no 10255 listener even for authorized clients. - - -The cluster equivalents of this repository's rules: nothing reaches a workload except through the TLS-terminating entry point (a Gateway API `Gateway`), and no Service becomes public through a casual `type: LoadBalancer` or `NodePort`; the entry point's own Service is the only exception. - -**If you run ingress-nginx today, migrate.** Earlier versions of this guide built on ingress-nginx. The Kubernetes project retired it in March 2026: per the Kubernetes Steering and Security Response Committees, "there will be no more releases for bug fixes, security patches, or any updates of any kind after the project is retired", and "choosing to remain with Ingress NGINX after its retirement leaves you and your users vulnerable to attack" (as of September 2026; statement linked in Sources). Detect it with cluster-admin permissions: `kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx`. Any pod returned means migration is required; the `nginx.ingress.kubernetes.io/*` annotations die with the controller. Kubernetes recommends Gateway API over Ingress (whose API is now frozen) and links a migration guide from its Gateway API page. The rest of this guide is the Gateway API form of the old rules. - -## 1. Gateway API with a maintained implementation - -Gateway API is a set of CRDs, not part of core Kubernetes; a controller you install implements them. This guide uses [Envoy Gateway](https://gateway.envoyproxy.io/): `helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.9.1 -n envoy-gateway-system --create-namespace` (version current at the time of writing; the default chart also installs the Gateway API CRDs). The chart does not create a `GatewayClass`; the quickstart applies one separately, and the `Gateway` below refers to it by name, so apply it first and confirm that the controller accepted it: +Gateway API is a set of CRDs, not part of core Kubernetes; a controller you install implements them. This guide uses [Envoy Gateway](https://gateway.envoyproxy.io/): `helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.9.1 -n envoy-gateway-system --create-namespace` (version current at the time of writing; the default chart also installs the Gateway API CRDs). The chart does not create a `GatewayClass`; the quickstart applies one separately, and the `Gateway` below refers to it by name, so apply it first and confirm that the controller accepted it: ```yaml apiVersion: gateway.networking.k8s.io/v1 @@ -719,3 +424,324 @@ allowed ranges out of the provider's own configuration rather than inferring the - AKS API server authorized IP ranges: https://learn.microsoft.com/en-us/azure/aks/api-server-authorized-ip-ranges - cert-manager Gateway API usage (enabling support, annotations; since cert-manager 1.15, Gateway API support no longer requires a feature gate but still requires explicit enablement): https://cert-manager.io/docs/usage/gateway/ ; ACME HTTP-01 `gatewayHTTPRoute` solver: https://cert-manager.io/docs/configuration/acme/http01/ - Traefik Kubernetes Gateway API provider: https://doc.traefik.io/traefik/reference/install-configuration/providers/kubernetes/kubernetes-gateway/ ; Cilium Gateway API support: https://docs.cilium.io/en/stable/network/servicemesh/gateway-api/gateway-api/ + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/kubernetes.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- kubelet: Kubernetes kubelet/kubeadm source (v1.37.1) +- kubernetes: Kubernetes documentation (unknown) +- gateway: Gateway API documentation (unknown) +- envoy: Envoy Gateway (v1.9.1) +- cert-manager: cert-manager Gateway support minimum (1.15) +- apache: Apache htpasswd documentation (unknown) +- authelia: Authelia documentation (unknown) +- eks: Amazon EKS documentation (unknown) +- eks-ami: Amazon EKS AMI bootstrap (6caf8311a3c6a8da71ac7e5e83f9c2e06287039a) +- gke: GKE documentation (unknown) +- gke-min: GKE new-cluster read-only-port boundary (1.32) +- aks: AKS documentation (unknown) +- traefik: Traefik documentation (unknown) +- cilium: Cilium documentation (unknown) +- envoy-rolling: Envoy Gateway documentation (rolling) (unknown) +- curl: curl documentation (unknown) +- nmap: Nmap documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- ingress-migration (REASONED; kubernetes): ingress-nginx retired in March 2026 with no subsequent updates; detect its labeled pods and migrate. Gateway API is recommended and Ingress API is frozen. +- gateway-install (REASONED; envoy, gateway, envoy-rolling): Install Envoy Gateway chart v1.9.1 and its default Gateway CRDs; separately create GatewayClass eg with the documented controllerName and confirm Accepted=True. +- gateway-alternatives (REASONED; traefik, cilium): Traefik uses providers.kubernetesGateway; Cilium uses gatewayAPI.enabled=true and requires kube-proxy replacement. +- gateway-tls (REASONED; gateway, envoy-rolling): Gateway HTTPS listener 443 terminates TLS using app-tls for app.example.com; HTTP 80 is for redirect and ACME challenges. +- gateway-route (REASONED; gateway): HTTPRoute app attaches only to the HTTPS listener via sectionName and forwards to the ClusterIP app Service on port 80. +- gateway-redirect (REASONED; gateway, envoy-rolling): The HTTP route uses RequestRedirect with scheme https and statusCode 301. +- cert-manager-enable (REASONED; cert-manager): From cert-manager 1.15, enable config.gatewayAPI.enabled; install Gateway CRDs before startup or restart cert-manager afterwards. +- cert-manager-issue (REASONED; cert-manager): Gateway issuer annotations produce Certificates per HTTPS-listener Secret using listener hostnames; ACME gatewayHTTPRoute parentRefs selects the challenge Gateway. +- basic-auth-policy (REASONED; gateway, envoy-rolling): Gateway API has no standard auth filter; Envoy SecurityPolicy attaches Basic auth to Gateway/HTTPRoute/GRPCRoute using an htpasswd Secret. +- basic-auth-hash (REASONED; apache, envoy-rolling): Envoy supports SHA hashes only; htpasswd -s is weak SHA-1, so use long random passwords over TLS and keep application login. +- basic-auth-input (REASONED; apache, envoy-rolling): htpasswd -i reads stdin, unlike argv-exposing -b; confirm twice, create with -cis, verify with -vi and only then create the Kubernetes Secret. +- oidc (REASONED; envoy-rolling): Envoy SecurityPolicy oidc uses issuer, clientID, clientSecret and redirectURL; enforce MFA at the identity provider. +- external-auth (REASONED; authelia, envoy-rolling): Authelia is an authorization endpoint, not a traffic proxy; Envoy extAuth.http targets its Service and /api/authz/ext-authz/. +- workload-exposure (REASONED; envoy-rolling, kubernetes): Expose workloads only through the Gateway LoadBalancer; keep databases on ClusterIP without routes and enforce pod access with NetworkPolicies plus database TLS/auth. +- secrets (REASONED; kubernetes): Store credentials in Secrets or an external operator, not committed ConfigMaps or environment literals; kubeconfigs containing credentials also need secret handling. +- eks-endpoint (REASONED; eks): EKS API defaults public; restrict public CIDRs or use private access. Private-only endpoints can resolve publicly to private VPC addresses. +- gke-endpoints (REASONED; gke): GKE authorized networks constrain IP-based access, not its separately IAM-gated DNS endpoint; inspect both and disable unused DNS access. +- aks-endpoint (REASONED; aks): Restrict AKS API access with authorized IP ranges; workload Gateway protection does not secure the control plane. +- kubeconfig (REASONED; kubernetes): Embedded tokens or client keys confer their RBAC access without another factor; exec-plugin configs may be only pointers, but args/env can themselves hold credentials. +- api-port (REASONED; kubernetes, eks): API server commonly uses 6443, but providers may use 443; use the complete kubeconfig endpoint and its actual port. +- etcd-ports (REASONED; kubernetes): etcd client/peer defaults are 2379/2380; require certificates, restrict holders and private access, and retain loopback listeners used by the local API server. +- etcd-storage (REASONED; kubernetes): API resources default to plaintext storage in etcd; valid client certificates are the boundary and etcd access is equivalent to cluster root. +- scheduler-port (REASONED; kubernetes): Scheduler default 10259 must remain restricted to cluster clients. +- controller-port (REASONED; kubernetes): Controller-manager default 10257 must remain restricted to cluster clients. +- proxy-port (REASONED; kubernetes): Worker kube-proxy health default 10256 needs separate node exposure checks. +- nodeports (REASONED; kubernetes): NodePort defaults span 30000 to 32767 TCP/UDP; Service listings do not actively probe that range. +- kubelet-legacy-anonymous (REASONED; kubelet): Legacy kubelet flags default anonymous-auth=true; explicitly disable it where flags control authentication. +- kubelet-legacy-webhook (REASONED; kubelet): Legacy authentication-token-webhook defaults false; explicitly enable it where flags configure authentication. +- kubelet-legacy-authz (REASONED; kubelet, kubernetes): Legacy authorization-mode defaults AlwaysAllow; set Webhook where flags configure authorization. +- kubelet-file-anonymous (REASONED; kubelet): v1beta1 KubeletConfiguration instead defaults authentication.anonymous.enabled=false. +- kubelet-file-webhook (REASONED; kubelet): v1beta1 KubeletConfiguration defaults authentication.webhook.enabled=true. +- kubelet-file-authz (REASONED; kubelet): v1beta1 KubeletConfiguration defaults authorization.mode=Webhook. +- kubelet-precedence (REASONED; kubelet): Explicit flags override --config; --config-dir drop-ins override defaults and the file. Inspect all effective settings together. +- kubeadm (REASONED; kubelet): kubeadm sets anonymous false, token webhook true, Webhook authorization and readOnlyPort 0; user overrides are preserved with warnings. +- eks-bootstrap (REASONED; eks-ami): The pinned EKS AMI bootstrap writes Anonymous.Enabled=false, Mode=Webhook and ReadOnlyPort=0; do not generalize to all managed nodes. +- kubelet-bind (REASONED; kubelet): Secured kubelet defaults to address 0.0.0.0 on 10250; wildcard help includes both families. Bind private addresses and restrict authorized callers. +- kubelet-exec (REASONED; kubelet): 10250 debugging handlers can execute in containers; authentication does not justify public reachability. +- readonly-default (REASONED; kubelet): Legacy read-only port defaults 10255, while v1beta1 readOnlyPort defaults 0; explicitly disable it and remove enabling overrides. +- readonly-auth (REASONED; kubelet): Read-only server uses plain HTTP with no authentication or authorization filter; its caller-supplied bind wiring was outside the inspected source subset. +- readonly-paths (REASONED; kubelet): Read-only handlers expose /pods, /stats/summary, metrics variants and health; v1.37.1 has no /spec handler. Securing 10250 does not secure 10255. +- gke-readonly (REASONED; gke-min): GKE disables the read-only port by default only for new clusters running 1.32+; inspect effective configuration on older or upgraded clusters. +- verify-services (REASONED; kubernetes, envoy-rolling, cert-manager): Only the Gateway should have NodePort/LoadBalancer exposure; verify its public address/DNS and Certificate Ready=True. +- verify-redirect (REASONED; gateway): HTTP should return 301 to the HTTPS application URL. +- verify-basic (REASONED; curl, envoy-rolling): Anonymous Basic-protected access should return 401; require SecurityPolicy Accepted=True and a valid credential reaching the app without the gateway Basic challenge. +- verify-api-target (REASONED; kubernetes): Use the whole kubeconfig endpoint, preserving IPv6 and inferring a schemeless endpoint from both cluster and user TLS settings, not a hardcoded 6443. +- verify-proxies (REASONED; curl): Disable curl config and configured proxies for direct probes; a transparent TLS middlebox can still answer for an unreachable target. +- verify-api-tcp (REASONED; kubernetes, eks): Outside allowed ranges, nc success proves something accepted TCP; corroborate failed probes with provider ranges and multiple vantage points. +- verify-api-http (REASONED; curl, eks): Any HTTP status with curl exit 0 means answered; exit 60 means a TLS peer answered, 7 failed connect, 28 is ambiguous timeout and 6 is DNS failure, not privacy proof. +- verify-node-scan (REASONED; kubernetes, nmap): Scan control-plane hosts first and every public node address in both families; workers alone miss API/etcd and separate etcd hosts are absent from kubectl nodes. +- verify-kubelet (REASONED; kubelet): Untrusted clients should reach neither kubelet port after isolation, while authorized clients still reach 10250; readOnlyPort=0 should leave no 10255 listener even for authorized clients. + +````json +{ + "schema": 1, + "checked": "2026-09-27", + "documentation_checked": "2026-09", + "body_sha256": "1e80b5ac6d9df2701bae1929db0d6cb2f9ce6416a58415b189d509640b8a44c8", + "components": { + "kubelet": { + "name": "Kubernetes kubelet/kubeadm source", + "basis": "v1.37.1", + "sources": { + "sc6056d6413a0": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L196-L224", + "s227d2a259fd3": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L373-L398", + "s568cd52cc674": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/apis/config/v1beta1/defaults.go#L85-L102", + "s2daf96dabbdf": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L281", + "s24ba323c3dab": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L282", + "sd81692a1876f": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L33-L48", + "s0eb30acf2f46": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L95-L100", + "s1c86aaef7307": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L144-L188", + "sb98bd327db92": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/utils.go#L66-L71", + "s3fc405855b4a": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/staging/src/k8s.io/kubelet/config/v1beta1/types.go#L160-L176", + "s98cf98130201": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L181-L250", + "s8bea69523ed9": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L339-L375", + "sa02a895a475c": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/cluster/ports/ports.go#L32-L37", + "s6b753b247f8a": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L477-L572", + "sd00d4cdb9276": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/stats/handler.go#L109-L133", + "s02441dcae3cf": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L577-L610" + } + }, + "kubernetes": { + "name": "Kubernetes documentation", + "basis": "unknown", + "sources": { + "s0f11d6ed39b7": "https://kubernetes.io/blog/2026/01/29/ingress-nginx-statement/", + "sca23283c5a9b": "https://kubernetes.io/docs/concepts/services-networking/gateway/", + "sd8c8df3b9ddc": "https://kubernetes.io/docs/concepts/services-networking/ingress/", + "se41a53ea6778": "https://kubernetes.io/docs/reference/config-api/kubeconfig.v1/", + "sae633285120e": "https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/", + "sd59fb2f35cd7": "https://kubernetes.io/docs/reference/networking/ports-and-protocols/", + "s01f081fed0ff": "https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/", + "s49a29258961b": "https://kubernetes.io/docs/reference/access-authn-authz/kubelet-authn-authz/", + "s04f0c3099fd7": "https://kubernetes.io/docs/reference/kubectl/jsonpath/" + } + }, + "gateway": { + "name": "Gateway API documentation", + "basis": "unknown", + "sources": { + "se0e45c61dc65": "https://gateway-api.sigs.k8s.io/guides/getting-started/introduction/", + "se1f28d98d168": "https://gateway-api.sigs.k8s.io/guides/user-guides/tls/", + "s4a971425e1d1": "https://gateway-api.sigs.k8s.io/guides/user-guides/http-routing/", + "s8d02fbc742ba": "https://gateway-api.sigs.k8s.io/guides/user-guides/http-redirect-rewrite/" + } + }, + "envoy": { + "name": "Envoy Gateway", + "basis": "v1.9.1", + "sources": { + "s6412abbdfdbd": "https://github.com/envoyproxy/gateway/releases/download/v1.9.1/quickstart.yaml" + } + }, + "cert-manager": { + "name": "cert-manager Gateway support minimum", + "basis": "1.15", + "sources": { + "s72bdb97928a8": "https://cert-manager.io/docs/usage/gateway/", + "s8d82244c2d6d": "https://cert-manager.io/docs/configuration/acme/http01/" + } + }, + "apache": { + "name": "Apache htpasswd documentation", + "basis": "unknown", + "sources": { + "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" + } + }, + "authelia": { + "name": "Authelia documentation", + "basis": "unknown", + "sources": { + "s0cb4f49871b5": "https://www.authelia.com/integration/proxies/introduction/", + "scfa9e2cc2ef5": "https://www.authelia.com/integration/kubernetes/envoy/gateway/" + } + }, + "eks": { + "name": "Amazon EKS documentation", + "basis": "unknown", + "sources": { + "s9724ce882930": "https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html" + } + }, + "eks-ami": { + "name": "Amazon EKS AMI bootstrap", + "basis": "6caf8311a3c6a8da71ac7e5e83f9c2e06287039a", + "sources": { + "sbe1fce93c7b2": "https://github.com/awslabs/amazon-eks-ami/blob/6caf8311a3c6a8da71ac7e5e83f9c2e06287039a/nodeadm/internal/kubelet/config.go" + } + }, + "gke": { + "name": "GKE documentation", + "basis": "unknown", + "sources": { + "s0f44d89575ed": "https://docs.cloud.google.com/kubernetes-engine/docs/concepts/network-isolation#how_authorized_networks_work" + } + }, + "gke-min": { + "name": "GKE new-cluster read-only-port boundary", + "basis": "1.32", + "sources": { + "s4c7be4adcab4": "https://docs.cloud.google.com/kubernetes-engine/docs/how-to/disable-kubelet-readonly-port" + } + }, + "aks": { + "name": "AKS documentation", + "basis": "unknown", + "sources": { + "s897dd27ab9a1": "https://learn.microsoft.com/en-us/azure/aks/api-server-authorized-ip-ranges" + } + }, + "traefik": { + "name": "Traefik documentation", + "basis": "unknown", + "sources": { + "s06f2ee45331e": "https://doc.traefik.io/traefik/reference/install-configuration/providers/kubernetes/kubernetes-gateway/" + } + }, + "cilium": { + "name": "Cilium documentation", + "basis": "unknown", + "sources": { + "sacaa7a78461d": "https://docs.cilium.io/en/stable/network/servicemesh/gateway-api/gateway-api/" + } + }, + "envoy-rolling": { + "name": "Envoy Gateway documentation (rolling)", + "basis": "unknown", + "sources": { + "se833317feea7": "https://gateway.envoyproxy.io/docs/tasks/security/secure-gateways/", + "sb5fa4f4ffa1a": "https://gateway.envoyproxy.io/docs/tasks/security/basic-auth/", + "s7fdd76c5d549": "https://gateway.envoyproxy.io/docs/tasks/security/oidc/", + "s100049c74adf": "https://gateway.envoyproxy.io/docs/tasks/security/ext-auth/", + "s9b1d646cb718": "https://gateway.envoyproxy.io/docs/tasks/traffic/http-redirect/", + "s881f08174c54": "https://gateway.envoyproxy.io/v1.9/install/install-helm/", + "s9ad0f418b20b": "https://gateway.envoyproxy.io/v1.9/tasks/quickstart/" + } + }, + "curl": { + "name": "curl documentation", + "basis": "unknown", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html", + "s7d4a6e627b1b": "https://curl.se/libcurl/c/libcurl-errors.html" + } + }, + "nmap": { + "name": "Nmap documentation", + "basis": "unknown", + "sources": { + "sb7d0e7eb5141": "https://nmap.org/book/man-host-discovery.html", + "s1818abeb3f92": "https://nmap.org/book/man-port-specification.html", + "s79c95712dd8d": "https://nmap.org/book/man-misc-options.html" + } + } + }, + "claims": { + "ingress-migration": {"text": "ingress-nginx retired in March 2026 with no subsequent updates; detect its labeled pods and migrate. Gateway API is recommended and Ingress API is frozen.", "components": ["kubernetes"], "sources": ["kubernetes:s0f11d6ed39b7", "kubernetes:sca23283c5a9b", "kubernetes:sd8c8df3b9ddc"], "status": "REASONED"}, + "gateway-install": {"text": "Install Envoy Gateway chart v1.9.1 and its default Gateway CRDs; separately create GatewayClass eg with the documented controllerName and confirm Accepted=True.", "components": ["envoy", "gateway", "envoy-rolling"], "sources": ["envoy-rolling:s881f08174c54", "envoy:s6412abbdfdbd", "gateway:se0e45c61dc65"], "status": "REASONED"}, + "gateway-alternatives": {"text": "Traefik uses providers.kubernetesGateway; Cilium uses gatewayAPI.enabled=true and requires kube-proxy replacement.", "components": ["traefik", "cilium"], "sources": ["traefik:s06f2ee45331e", "cilium:sacaa7a78461d"], "status": "REASONED"}, + "gateway-tls": {"text": "Gateway HTTPS listener 443 terminates TLS using app-tls for app.example.com; HTTP 80 is for redirect and ACME challenges.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:se1f28d98d168", "envoy-rolling:se833317feea7"], "status": "REASONED"}, + "gateway-route": {"text": "HTTPRoute app attaches only to the HTTPS listener via sectionName and forwards to the ClusterIP app Service on port 80.", "components": ["gateway"], "sources": ["gateway:s4a971425e1d1"], "status": "REASONED"}, + "gateway-redirect": {"text": "The HTTP route uses RequestRedirect with scheme https and statusCode 301.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:s8d02fbc742ba", "envoy-rolling:s9b1d646cb718"], "status": "REASONED"}, + "cert-manager-enable": {"text": "From cert-manager 1.15, enable config.gatewayAPI.enabled; install Gateway CRDs before startup or restart cert-manager afterwards.", "components": ["cert-manager"], "sources": ["cert-manager:s72bdb97928a8"], "status": "REASONED"}, + "cert-manager-issue": {"text": "Gateway issuer annotations produce Certificates per HTTPS-listener Secret using listener hostnames; ACME gatewayHTTPRoute parentRefs selects the challenge Gateway.", "components": ["cert-manager"], "sources": ["cert-manager:s72bdb97928a8", "cert-manager:s8d82244c2d6d"], "status": "REASONED"}, + "basic-auth-policy": {"text": "Gateway API has no standard auth filter; Envoy SecurityPolicy attaches Basic auth to Gateway/HTTPRoute/GRPCRoute using an htpasswd Secret.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:s4a971425e1d1", "envoy-rolling:sb5fa4f4ffa1a"], "status": "REASONED"}, + "basic-auth-hash": {"text": "Envoy supports SHA hashes only; htpasswd -s is weak SHA-1, so use long random passwords over TLS and keep application login.", "components": ["apache", "envoy-rolling"], "sources": ["envoy-rolling:sb5fa4f4ffa1a", "apache:scde9004bb967"], "status": "REASONED"}, + "basic-auth-input": {"text": "htpasswd -i reads stdin, unlike argv-exposing -b; confirm twice, create with -cis, verify with -vi and only then create the Kubernetes Secret.", "components": ["apache", "envoy-rolling"], "sources": ["apache:scde9004bb967", "envoy-rolling:sb5fa4f4ffa1a"], "status": "REASONED"}, + "oidc": {"text": "Envoy SecurityPolicy oidc uses issuer, clientID, clientSecret and redirectURL; enforce MFA at the identity provider.", "components": ["envoy-rolling"], "sources": ["envoy-rolling:s7fdd76c5d549"], "status": "REASONED"}, + "external-auth": {"text": "Authelia is an authorization endpoint, not a traffic proxy; Envoy extAuth.http targets its Service and /api/authz/ext-authz/.", "components": ["authelia", "envoy-rolling"], "sources": ["authelia:s0cb4f49871b5", "authelia:scfa9e2cc2ef5", "envoy-rolling:s100049c74adf"], "status": "REASONED"}, + "workload-exposure": {"text": "Expose workloads only through the Gateway LoadBalancer; keep databases on ClusterIP without routes and enforce pod access with NetworkPolicies plus database TLS/auth.", "components": ["envoy-rolling", "kubernetes"], "sources": ["envoy-rolling:s9ad0f418b20b", "kubernetes:sca23283c5a9b"], "status": "REASONED"}, + "secrets": {"text": "Store credentials in Secrets or an external operator, not committed ConfigMaps or environment literals; kubeconfigs containing credentials also need secret handling.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778", "kubernetes:sae633285120e"], "status": "REASONED"}, + "eks-endpoint": {"text": "EKS API defaults public; restrict public CIDRs or use private access. Private-only endpoints can resolve publicly to private VPC addresses.", "components": ["eks"], "sources": ["eks:s9724ce882930"], "status": "REASONED"}, + "gke-endpoints": {"text": "GKE authorized networks constrain IP-based access, not its separately IAM-gated DNS endpoint; inspect both and disable unused DNS access.", "components": ["gke"], "sources": ["gke:s0f44d89575ed"], "status": "REASONED"}, + "aks-endpoint": {"text": "Restrict AKS API access with authorized IP ranges; workload Gateway protection does not secure the control plane.", "components": ["aks"], "sources": ["aks:s897dd27ab9a1"], "status": "REASONED"}, + "kubeconfig": {"text": "Embedded tokens or client keys confer their RBAC access without another factor; exec-plugin configs may be only pointers, but args/env can themselves hold credentials.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778"], "status": "REASONED"}, + "api-port": {"text": "API server commonly uses 6443, but providers may use 443; use the complete kubeconfig endpoint and its actual port.", "components": ["kubernetes", "eks"], "sources": ["kubernetes:sd59fb2f35cd7", "eks:s9724ce882930"], "status": "REASONED"}, + "etcd-ports": {"text": "etcd client/peer defaults are 2379/2380; require certificates, restrict holders and private access, and retain loopback listeners used by the local API server.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7", "kubernetes:s01f081fed0ff"], "status": "REASONED"}, + "etcd-storage": {"text": "API resources default to plaintext storage in etcd; valid client certificates are the boundary and etcd access is equivalent to cluster root.", "components": ["kubernetes"], "sources": ["kubernetes:sae633285120e", "kubernetes:s01f081fed0ff"], "status": "REASONED"}, + "scheduler-port": {"text": "Scheduler default 10259 must remain restricted to cluster clients.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, + "controller-port": {"text": "Controller-manager default 10257 must remain restricted to cluster clients.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, + "proxy-port": {"text": "Worker kube-proxy health default 10256 needs separate node exposure checks.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, + "nodeports": {"text": "NodePort defaults span 30000 to 32767 TCP/UDP; Service listings do not actively probe that range.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, + "kubelet-legacy-anonymous": {"text": "Legacy kubelet flags default anonymous-auth=true; explicitly disable it where flags control authentication.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0", "kubelet:s227d2a259fd3"], "status": "REASONED"}, + "kubelet-legacy-webhook": {"text": "Legacy authentication-token-webhook defaults false; explicitly enable it where flags configure authentication.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0"], "status": "REASONED"}, + "kubelet-legacy-authz": {"text": "Legacy authorization-mode defaults AlwaysAllow; set Webhook where flags configure authorization.", "components": ["kubelet", "kubernetes"], "sources": ["kubelet:sc6056d6413a0", "kubernetes:s49a29258961b"], "status": "REASONED"}, + "kubelet-file-anonymous": {"text": "v1beta1 KubeletConfiguration instead defaults authentication.anonymous.enabled=false.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, + "kubelet-file-webhook": {"text": "v1beta1 KubeletConfiguration defaults authentication.webhook.enabled=true.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, + "kubelet-file-authz": {"text": "v1beta1 KubeletConfiguration defaults authorization.mode=Webhook.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, + "kubelet-precedence": {"text": "Explicit flags override --config; --config-dir drop-ins override defaults and the file. Inspect all effective settings together.", "components": ["kubelet"], "sources": ["kubelet:s2daf96dabbdf", "kubelet:s24ba323c3dab"], "status": "REASONED"}, + "kubeadm": {"text": "kubeadm sets anonymous false, token webhook true, Webhook authorization and readOnlyPort 0; user overrides are preserved with warnings.", "components": ["kubelet"], "sources": ["kubelet:sd81692a1876f", "kubelet:s0eb30acf2f46", "kubelet:s1c86aaef7307", "kubelet:sb98bd327db92"], "status": "REASONED"}, + "eks-bootstrap": {"text": "The pinned EKS AMI bootstrap writes Anonymous.Enabled=false, Mode=Webhook and ReadOnlyPort=0; do not generalize to all managed nodes.", "components": ["eks-ami"], "sources": ["eks-ami:sbe1fce93c7b2"], "status": "REASONED"}, + "kubelet-bind": {"text": "Secured kubelet defaults to address 0.0.0.0 on 10250; wildcard help includes both families. Bind private addresses and restrict authorized callers.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674", "kubelet:s3fc405855b4a", "kubelet:s227d2a259fd3", "kubelet:s98cf98130201"], "status": "REASONED"}, + "kubelet-exec": {"text": "10250 debugging handlers can execute in containers; authentication does not justify public reachability.", "components": ["kubelet"], "sources": ["kubelet:s8bea69523ed9", "kubelet:s3fc405855b4a", "kubelet:s98cf98130201", "kubelet:s02441dcae3cf"], "status": "REASONED"}, + "readonly-default": {"text": "Legacy read-only port defaults 10255, while v1beta1 readOnlyPort defaults 0; explicitly disable it and remove enabling overrides.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0", "kubelet:sa02a895a475c", "kubelet:s3fc405855b4a"], "status": "REASONED"}, + "readonly-auth": {"text": "Read-only server uses plain HTTP with no authentication or authorization filter; its caller-supplied bind wiring was outside the inspected source subset.", "components": ["kubelet"], "sources": ["kubelet:s98cf98130201", "kubelet:s8bea69523ed9"], "status": "REASONED"}, + "readonly-paths": {"text": "Read-only handlers expose /pods, /stats/summary, metrics variants and health; v1.37.1 has no /spec handler. Securing 10250 does not secure 10255.", "components": ["kubelet"], "sources": ["kubelet:s6b753b247f8a", "kubelet:sd00d4cdb9276"], "status": "REASONED"}, + "gke-readonly": {"text": "GKE disables the read-only port by default only for new clusters running 1.32+; inspect effective configuration on older or upgraded clusters.", "components": ["gke-min"], "sources": ["gke-min:s4c7be4adcab4"], "status": "REASONED"}, + "verify-services": {"text": "Only the Gateway should have NodePort/LoadBalancer exposure; verify its public address/DNS and Certificate Ready=True.", "components": ["kubernetes", "envoy-rolling", "cert-manager"], "sources": ["kubernetes:s04f0c3099fd7", "envoy-rolling:s9ad0f418b20b", "cert-manager:s72bdb97928a8"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP should return 301 to the HTTPS application URL.", "components": ["gateway"], "sources": ["gateway:s8d02fbc742ba"], "status": "REASONED", "verify": [1]}, + "verify-basic": {"text": "Anonymous Basic-protected access should return 401; require SecurityPolicy Accepted=True and a valid credential reaching the app without the gateway Basic challenge.", "components": ["curl", "envoy-rolling"], "sources": ["envoy-rolling:sb5fa4f4ffa1a", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, + "verify-api-target": {"text": "Use the whole kubeconfig endpoint, preserving IPv6 and inferring a schemeless endpoint from both cluster and user TLS settings, not a hardcoded 6443.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778", "kubernetes:sd59fb2f35cd7"], "status": "REASONED", "verify": [1]}, + "verify-proxies": {"text": "Disable curl config and configured proxies for direct probes; a transparent TLS middlebox can still answer for an unreachable target.", "components": ["curl"], "sources": ["curl:s2b2686afaf41", "curl:s7d4a6e627b1b"], "status": "REASONED", "verify": [1]}, + "verify-api-tcp": {"text": "Outside allowed ranges, nc success proves something accepted TCP; corroborate failed probes with provider ranges and multiple vantage points.", "components": ["kubernetes", "eks"], "sources": ["kubernetes:sd59fb2f35cd7", "eks:s9724ce882930"], "status": "REASONED", "verify": [1]}, + "verify-api-http": {"text": "Any HTTP status with curl exit 0 means answered; exit 60 means a TLS peer answered, 7 failed connect, 28 is ambiguous timeout and 6 is DNS failure, not privacy proof.", "components": ["curl", "eks"], "sources": ["curl:s7d4a6e627b1b", "eks:s9724ce882930"], "status": "REASONED", "verify": [1]}, + "verify-node-scan": {"text": "Scan control-plane hosts first and every public node address in both families; workers alone miss API/etcd and separate etcd hosts are absent from kubectl nodes.", "components": ["kubernetes", "nmap"], "sources": ["kubernetes:sd59fb2f35cd7", "kubernetes:s01f081fed0ff", "nmap:sb7d0e7eb5141", "nmap:s1818abeb3f92", "nmap:s79c95712dd8d"], "status": "REASONED", "verify": [1]}, + "verify-kubelet": {"text": "Untrusted clients should reach neither kubelet port after isolation, while authorized clients still reach 10250; readOnlyPort=0 should leave no 10255 listener even for authorized clients.", "components": ["kubelet"], "sources": ["kubelet:s98cf98130201", "kubelet:s3fc405855b4a"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/lighttpd.md b/plugin/skills/secure-deployment-config/references/lighttpd.md index 6c885769e..08de50591 100644 --- a/plugin/skills/secure-deployment-config/references/lighttpd.md +++ b/plugin/skills/secure-deployment-config/references/lighttpd.md @@ -1,213 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-19", - "documentation_checked": "2026-09", - "body_sha256": "aa010c3e401699d0b9ff9541720750e57529d097494238fca31e5db6a0c3826f", - "components": { - "tls": { - "name": "lighttpd TLS minimum", - "basis": "1.4.56", - "sources": { - "sbff3cb2e01a8": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_SSL" - } - }, - "key": { - "name": "lighttpd separate-key minimum", - "basis": "1.4.53", - "sources": { - "sbff3cb2e01a8": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_SSL" - } - }, - "redirect": { - "name": "lighttpd redirect default transition", - "basis": "1.4.75", - "sources": { - "s6de629323c64": "https://redmine.lighttpd.net/projects/lighttpd/wiki/HowToRedirectHttpToHttps" - } - }, - "docs": { - "name": "lighttpd documentation", - "basis": "unknown", - "sources": { - "s1dbd1ce27da5": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Mod_auth", - "s6d5666b862a0": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ConfigurationOptions", - "s6de629323c64": "https://redmine.lighttpd.net/projects/lighttpd/wiki/HowToRedirectHttpToHttps" - } - }, - "modules-source": { - "name": "lighttpd module loader source", - "basis": "1.4.75", - "sources": { - "s8680e561bed2": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L395-L544", - "s8e9688979427": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L270-L290", - "s9bd716d27e8d": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L382-L393", - "s21288573041a": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L662-L945", - "s67bb3733d81c": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L1684-L1711" - } - }, - "authn-source": { - "name": "lighttpd file authentication source", - "basis": "1.4.75", - "sources": { - "s2c11c0fbc0ca": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_authn_file.c#L340-L397", - "se971effb489f": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_authn_file.c#L613-L740" - } - }, - "htpasswd-tool": { - "name": "Apache htpasswd manual", - "basis": "2.4", - "sources": { - "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" - } - }, - "debian-package": { - "name": "Debian apache2-utils package", - "basis": "trixie", - "sources": { - "s436bdf5f1fb9": "https://packages.debian.org/trixie/apache2-utils" - } - }, - "authelia": { - "name": "Authelia proxy support", - "basis": "v4.39.28", - "sources": { - "s77083bc8faa2": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L24-L36" - } - }, - "cloudflare-access": { - "name": "Cloudflare Access documentation", - "basis": "commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104", - "sources": { - "sfddb92e438ca": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L14-L16", - "s7cafe1b17afd": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54", - "s603bdb6b24ab": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L45-L45", - "s863803bd9a1f": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L48-L48", - "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" - } - }, - "fedora-package": { - "name": "Fedora httpd-tools package", - "basis": "unknown", - "sources": { - "s919088d03a5e": "https://packages.fedoraproject.org/pkgs/httpd/httpd-tools/" - } - }, - "lighttpd-verify-source": { - "name": "lighttpd verify source", - "basis": "1.4.75", - "sources": { - "s0bbb4c861d5e": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/lighttpd.8#L22-L23", - "sca076bcd18dc": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/lighttpd.8#L33-L37", - "s9e69235fbbc7": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1439-L1451", - "s87104d328493": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1466-L1467", - "sba72bab46867": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1510-L1523", - "s2faa9b3149ac": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1942-L1953", - "s87b4b266bb0e": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L2320-L2384", - "sa989ef425f1d": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L229-L235", - "sf9d56b052c61": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/systemd/lighttpd.service#L5-L11", - "s6a29b290d83a": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/request.c#L423-L430", - "s2fc4cf82abd9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/request.c#L1235-L1247", - "s3f692a48bd01": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile-glue.c#L504-L508", - "sbb09703388c3": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L213-L223", - "s8bb6bd7774f9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_openssl.c#L1025-L1034", - "sb4c43fa1a807": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_openssl.c#L1294-L1330", - "s9e49b928cc81": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/response.c#L227-L228" - } - }, - "systemd-reload": { - "name": "systemd reload manual", - "basis": "v262", - "sources": { - "sb3f5069cd250": "https://github.com/systemd/systemd/blob/v262/man/systemctl.xml#L462-L474", - "s441757b7048f": "https://github.com/systemd/systemd/blob/v262/man/systemd.service.xml#L498-L519" - } - }, - "apache-vhosts": { - "name": "Apache virtual-host source", - "basis": "2.4.69", - "sources": { - "s558db45d4d61": "https://github.com/apache/httpd/blob/2.4.69/docs/manual/vhosts/details.xml#L217-L231" - } - }, - "release-news": { - "name": "lighttpd release NEWS", - "basis": "1.4.75", - "sources": { - "s4e990a9e5ec1": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1660-L1660", - "se97c93bf64a8": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1927-L1927", - "s1658d22c71f9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L2465-L2465", - "s95dd525fe4ac": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L2505-L2505", - "sc0d0162991d5": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L6-L7" - } - } - }, - "claims": { - "tls-defaults": {"text": "For this guide's 1.4.56-and-later scope, NEWS records the mod_openssl MinProtocol TLSv1.2 default in 1.4.56. Disabling SSLv2, SSLv3, TLS 1.0 and TLS 1.1 is this guide's reading of that floor; NEWS does not establish defaults for every later release.", "components": ["tls", "release-news"], "sources": ["tls:sbff3cb2e01a8", "release-news:s4e990a9e5ec1", "release-news:se97c93bf64a8"], "status": "REASONED"}, - "tls-listeners": {"text": "Load mod_openssl and enable TLS on both :443 and [::]:443 with ssl.pemfile and ssl.privkey; omitting the IPv6 socket leaves IPv6 clients on plain HTTP.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED"}, - "certificate-chain": {"text": "ssl.pemfile uses fullchain.pem rather than a certificate without its chain.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED"}, - "separate-key": {"text": "NEWS records optional mod_openssl ssl.privkey in 1.4.53. On older versions, concatenate certificate and key into ssl.pemfile (this guide's compatibility advice; the entry does not establish that procedure).", "components": ["key", "release-news"], "sources": ["key:sbff3cb2e01a8", "release-news:s1658d22c71f9", "release-news:s95dd525fe4ac"], "status": "REASONED"}, - "protocol-floor": {"text": "Set MinProtocol to TLSv1.2 with ssl.openssl.ssl-conf-cmd explicitly (this guide's advice); NEWS records that default for mod_openssl in 1.4.56, not every later release.", "components": ["tls", "release-news"], "sources": ["tls:sbff3cb2e01a8", "release-news:s4e990a9e5ec1", "release-news:se97c93bf64a8"], "status": "REASONED"}, - "module-defaults": {"text": "In lighttpd 1.4.75, mod_h2 is inserted unless server.feature-flags disables server.h2proto (enabled by default); when server.compat-module-load is enabled (the default), mod_indexfile and mod_dirlisting are conditionally added when enabled, mod_staticfile is added when absent, and compatibility handling can also add other modules, including authentication and TLS modules under their respective conditions. Load mod_redirect explicitly.", "components": ["docs", "modules-source"], "sources": ["docs:s6d5666b862a0", "modules-source:s8680e561bed2", "modules-source:s8e9688979427", "modules-source:s9bd716d27e8d", "modules-source:s21288573041a", "modules-source:s67bb3733d81c"], "status": "REASONED"}, - "http-redirect": {"text": "For the http scheme, redirect to HTTPS preserving authority, path and query; port 80 must redirect rather than serve content.", "components": ["docs"], "sources": ["docs:s6de629323c64"], "status": "REASONED"}, - "redirect-code": {"text": "NEWS records url.redirect-code = 308 as the new default in 1.4.75. Set 308 explicitly before 1.4.75 (this guide's advice; the entry does not specify each earlier default).", "components": ["redirect", "release-news"], "sources": ["redirect:s6de629323c64", "release-news:sc0d0162991d5"], "status": "REASONED"}, - "basic-auth": {"text": "Load mod_auth and mod_authn_file, select the htpasswd file backend and require valid-user Basic authentication on / over TLS; application login is preferable.", "components": ["docs"], "sources": ["docs:s1dbd1ce27da5"], "status": "REASONED"}, - "password-file": {"text": "Create the user file with Apache htpasswd (Debian apache2-utils; Fedora httpd-tools is this guide's package assessment); lighttpd 1.4.75 reads username:stored-hash entries and checks {SHA} SHA-1 and $apr1$ internally, with other hashes of at least 13 characters checked through crypt() when built with crypt support; under USE_LIB_CRYPTO_MD4, the $1+ntlm$ path derives an NTLM hash and calls crypt() with that derived value and a $1$ salt. Check the build and platform's accepted hashes before choosing a flag; this check is this guide's compatibility advice.", "components": ["docs", "authn-source", "htpasswd-tool", "debian-package", "fedora-package"], "sources": ["docs:s1dbd1ce27da5", "authn-source:s2c11c0fbc0ca", "authn-source:se971effb489f", "htpasswd-tool:scde9004bb967", "debian-package:s436bdf5f1fb9", "fedora-package:s919088d03a5e"], "status": "REASONED"}, - "mfa": {"text": "Basic authentication is single-factor; add MFA by fronting this service with Cloudflare Access or an MFA-capable proxy (this guide's requirement). Authelia v4.39.28's complete supported-proxy list has no lighttpd entry (this guide's inference from the table); Cloudflare Access documents self-hosted application and MFA policy controls; applying them to lighttpd is this guide's assessment. This guide assesses that a lighttpd origin reachable without passing through Access is not protected by it.", "components": ["docs", "authelia", "cloudflare-access"], "sources": ["docs:s1dbd1ce27da5", "authelia:s77083bc8faa2", "cloudflare-access:sfddb92e438ca", "cloudflare-access:s7cafe1b17afd", "cloudflare-access:sdcac732541da", "cloudflare-access:s603bdb6b24ab", "cloudflare-access:s863803bd9a1f"], "status": "REASONED"}, - "verify-config": {"text": "lighttpd 1.4.75 -tt -f /etc/lighttpd/lighttpd.conf tests the named file for syntax errors and initializes modules; configuration failures prevent the && reload (this guide's inference from the nonzero process result and shell chaining). The cited upstream unit starts from that file and defines a USR1 reload through ExecReload; systemctl reload triggers the service action, whose signal does not establish completion. Confirm the installed unit uses that file and defines reload (this guide's advice).", "components": ["docs", "lighttpd-verify-source", "systemd-reload"], "sources": ["docs:s6d5666b862a0", "lighttpd-verify-source:s0bbb4c861d5e", "lighttpd-verify-source:sca076bcd18dc", "lighttpd-verify-source:s9e69235fbbc7", "lighttpd-verify-source:s87104d328493", "lighttpd-verify-source:sba72bab46867", "lighttpd-verify-source:s2faa9b3149ac", "lighttpd-verify-source:s87b4b266bb0e", "lighttpd-verify-source:sa989ef425f1d", "lighttpd-verify-source:sf9d56b052c61", "systemd-reload:sb3f5069cd250", "systemd-reload:s441757b7048f"], "status": "REASONED", "verify": [1]}, - "verify-redirect": {"text": "The HTTP header request should show a redirect to HTTPS; these are read-and-judge status checks.", "components": ["docs"], "sources": ["docs:s6de629323c64"], "status": "REASONED", "verify": [1]}, - "verify-auth": {"text": "An HTTPS request without credentials should return 401 once authentication is enabled.", "components": ["docs"], "sources": ["docs:s1dbd1ce27da5"], "status": "REASONED", "verify": [1]}, - "verify-listeners": {"text": "Read every listener with ss -tlnp: front-door authentication does not establish that no other listener serves content. The ss version is unrecorded.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED", "verify": [1]}, - "verify-host": {"text": "In lighttpd 1.4.75, request-time $HTTP[\"host\"] matching uses the HTTP Host value, while TLS SNI can select mod_openssl settings before request headers; a Host outside a conditional auth.require falls back to global configuration for that rule (this guide's inference from conditional merging and mod_auth documentation). On a protected path, this guide interprets 401 as the pass and 200 as the protected path served without authentication on this TLS connection to a client that sends another host. Apache 2.4.69 uses the SNI hostname, when the client sends it and the server supports SNI, for name-based TLS virtual-host selection; varying Host while keeping URL and SNI fixed is this guide's auth-check advice.", "components": ["docs", "lighttpd-verify-source", "apache-vhosts"], "sources": ["docs:s1dbd1ce27da5", "lighttpd-verify-source:s6a29b290d83a", "lighttpd-verify-source:s2fc4cf82abd9", "lighttpd-verify-source:s3f692a48bd01", "lighttpd-verify-source:sbb09703388c3", "lighttpd-verify-source:s8bb6bd7774f9", "lighttpd-verify-source:sb4c43fa1a807", "lighttpd-verify-source:s9e49b928cc81", "apache-vhosts:s558db45d4d61"], "status": "REASONED", "verify": [1]} - } -} ---- # lighttpd: TLS and authentication - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +Written against lighttpd TLS minimum 1.4.56, lighttpd separate-key minimum 1.4.53, lighttpd redirect default transition 1.4.75, lighttpd module loader source 1.4.75, lighttpd file authentication source 1.4.75, lighttpd verify source 1.4.75, Apache htpasswd manual 2.4, and Debian apache2-utils package trixie, plus 5 more recorded bases; 2 further sources carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 16 claims, all reasoned from cited sources. -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- tls: lighttpd TLS minimum (1.4.56) -- key: lighttpd separate-key minimum (1.4.53) -- redirect: lighttpd redirect default transition (1.4.75) -- docs: lighttpd documentation (unknown) -- modules-source: lighttpd module loader source (1.4.75) -- authn-source: lighttpd file authentication source (1.4.75) -- htpasswd-tool: Apache htpasswd manual (2.4) -- debian-package: Debian apache2-utils package (trixie) -- authelia: Authelia proxy support (v4.39.28) -- cloudflare-access: Cloudflare Access documentation (commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- fedora-package: Fedora httpd-tools package (unknown) -- lighttpd-verify-source: lighttpd verify source (1.4.75) -- systemd-reload: systemd reload manual (v262) -- apache-vhosts: Apache virtual-host source (2.4.69) -- release-news: lighttpd release NEWS (1.4.75) - -Claims (full source mapping in the front matter): - -- tls-defaults (REASONED; tls, release-news): For this guide's 1.4.56-and-later scope, NEWS records the mod_openssl MinProtocol TLSv1.2 default in 1.4.56. Disabling SSLv2, SSLv3, TLS 1.0 and TLS 1.1 is this guide's reading of that floor; NEWS does not establish defaults for every later release. -- tls-listeners (REASONED; tls): Load mod_openssl and enable TLS on both :443 and [::]:443 with ssl.pemfile and ssl.privkey; omitting the IPv6 socket leaves IPv6 clients on plain HTTP. -- certificate-chain (REASONED; tls): ssl.pemfile uses fullchain.pem rather than a certificate without its chain. -- separate-key (REASONED; key, release-news): NEWS records optional mod_openssl ssl.privkey in 1.4.53. On older versions, concatenate certificate and key into ssl.pemfile (this guide's compatibility advice; the entry does not establish that procedure). -- protocol-floor (REASONED; tls, release-news): Set MinProtocol to TLSv1.2 with ssl.openssl.ssl-conf-cmd explicitly (this guide's advice); NEWS records that default for mod_openssl in 1.4.56, not every later release. -- module-defaults (REASONED; docs, modules-source): In lighttpd 1.4.75, mod_h2 is inserted unless server.feature-flags disables server.h2proto (enabled by default); when server.compat-module-load is enabled (the default), mod_indexfile and mod_dirlisting are conditionally added when enabled, mod_staticfile is added when absent, and compatibility handling can also add other modules, including authentication and TLS modules under their respective conditions. Load mod_redirect explicitly. -- http-redirect (REASONED; docs): For the http scheme, redirect to HTTPS preserving authority, path and query; port 80 must redirect rather than serve content. -- redirect-code (REASONED; redirect, release-news): NEWS records url.redirect-code = 308 as the new default in 1.4.75. Set 308 explicitly before 1.4.75 (this guide's advice; the entry does not specify each earlier default). -- basic-auth (REASONED; docs): Load mod_auth and mod_authn_file, select the htpasswd file backend and require valid-user Basic authentication on / over TLS; application login is preferable. -- password-file (REASONED; docs, authn-source, htpasswd-tool, debian-package, fedora-package): Create the user file with Apache htpasswd (Debian apache2-utils; Fedora httpd-tools is this guide's package assessment); lighttpd 1.4.75 reads username:stored-hash entries and checks {SHA} SHA-1 and $apr1$ internally, with other hashes of at least 13 characters checked through crypt() when built with crypt support; under USE_LIB_CRYPTO_MD4, the $1+ntlm$ path derives an NTLM hash and calls crypt() with that derived value and a $1$ salt. Check the build and platform's accepted hashes before choosing a flag; this check is this guide's compatibility advice. -- mfa (REASONED; docs, authelia, cloudflare-access): Basic authentication is single-factor; add MFA by fronting this service with Cloudflare Access or an MFA-capable proxy (this guide's requirement). Authelia v4.39.28's complete supported-proxy list has no lighttpd entry (this guide's inference from the table); Cloudflare Access documents self-hosted application and MFA policy controls; applying them to lighttpd is this guide's assessment. This guide assesses that a lighttpd origin reachable without passing through Access is not protected by it. -- verify-config (REASONED; docs, lighttpd-verify-source, systemd-reload): lighttpd 1.4.75 -tt -f /etc/lighttpd/lighttpd.conf tests the named file for syntax errors and initializes modules; configuration failures prevent the && reload (this guide's inference from the nonzero process result and shell chaining). The cited upstream unit starts from that file and defines a USR1 reload through ExecReload; systemctl reload triggers the service action, whose signal does not establish completion. Confirm the installed unit uses that file and defines reload (this guide's advice). -- verify-redirect (REASONED; docs): The HTTP header request should show a redirect to HTTPS; these are read-and-judge status checks. -- verify-auth (REASONED; docs): An HTTPS request without credentials should return 401 once authentication is enabled. -- verify-listeners (REASONED; tls): Read every listener with ss -tlnp: front-door authentication does not establish that no other listener serves content. The ss version is unrecorded. -- verify-host (REASONED; docs, lighttpd-verify-source, apache-vhosts): In lighttpd 1.4.75, request-time $HTTP["host"] matching uses the HTTP Host value, while TLS SNI can select mod_openssl settings before request headers; a Host outside a conditional auth.require falls back to global configuration for that rule (this guide's inference from conditional merging and mod_auth documentation). On a protected path, this guide interprets 401 as the pass and 200 as the protected path served without authentication on this TLS connection to a client that sends another host. Apache 2.4.69 uses the SNI hostname, when the client sends it and the server supports SNI, for name-based TLS virtual-host selection; varying Host while keeping URL and SNI fixed is this guide's auth-check advice. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Applies to lighttpd 1.4.56 and later. NEWS records the `mod_openssl` `MinProtocol TLSv1.2` default in its 1.4.56 release section (https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1660-L1660; https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1927-L1927). Disabling SSLv2/SSLv3/TLS 1.0/TLS 1.1 is this guide's reading of that floor; NEWS does not establish defaults for every later release. Get a certificate first: [free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md). @@ -361,3 +156,234 @@ curl -q -s -o /dev/null -w '%{http_code}\n' -H 'Host: not-configured.example' \ - Cloudflare Access origin reachability at commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L45-L45 ; https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L48-L48 - Cloudflare Access MFA policy controls at commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81 - Fedora `httpd-tools` package (checked October 2026): https://packages.fedoraproject.org/pkgs/httpd/httpd-tools/ + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/lighttpd.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- tls: lighttpd TLS minimum (1.4.56) +- key: lighttpd separate-key minimum (1.4.53) +- redirect: lighttpd redirect default transition (1.4.75) +- modules-source: lighttpd module loader source (1.4.75) +- authn-source: lighttpd file authentication source (1.4.75) +- lighttpd-verify-source: lighttpd verify source (1.4.75) +- docs: lighttpd documentation (unknown) +- htpasswd-tool: Apache htpasswd manual (2.4) +- debian-package: Debian apache2-utils package (trixie) +- authelia: Authelia proxy support (v4.39.28) +- cloudflare-access: Cloudflare Access documentation (commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- fedora-package: Fedora httpd-tools package (unknown) +- apache-vhosts: Apache virtual-host source (2.4.69) +- systemd-reload: systemd reload manual (v262) +- release-news: lighttpd release NEWS (1.4.75) + +Claims (full source mapping in the JSON block below): + +- tls-defaults (REASONED; tls, release-news): For this guide's 1.4.56-and-later scope, NEWS records the mod_openssl MinProtocol TLSv1.2 default in 1.4.56. Disabling SSLv2, SSLv3, TLS 1.0 and TLS 1.1 is this guide's reading of that floor; NEWS does not establish defaults for every later release. +- tls-listeners (REASONED; tls): Load mod_openssl and enable TLS on both :443 and [::]:443 with ssl.pemfile and ssl.privkey; omitting the IPv6 socket leaves IPv6 clients on plain HTTP. +- certificate-chain (REASONED; tls): ssl.pemfile uses fullchain.pem rather than a certificate without its chain. +- separate-key (REASONED; key, release-news): NEWS records optional mod_openssl ssl.privkey in 1.4.53. On older versions, concatenate certificate and key into ssl.pemfile (this guide's compatibility advice; the entry does not establish that procedure). +- protocol-floor (REASONED; tls, release-news): Set MinProtocol to TLSv1.2 with ssl.openssl.ssl-conf-cmd explicitly (this guide's advice); NEWS records that default for mod_openssl in 1.4.56, not every later release. +- module-defaults (REASONED; docs, modules-source): In lighttpd 1.4.75, mod_h2 is inserted unless server.feature-flags disables server.h2proto (enabled by default); when server.compat-module-load is enabled (the default), mod_indexfile and mod_dirlisting are conditionally added when enabled, mod_staticfile is added when absent, and compatibility handling can also add other modules, including authentication and TLS modules under their respective conditions. Load mod_redirect explicitly. +- http-redirect (REASONED; docs): For the http scheme, redirect to HTTPS preserving authority, path and query; port 80 must redirect rather than serve content. +- redirect-code (REASONED; redirect, release-news): NEWS records url.redirect-code = 308 as the new default in 1.4.75. Set 308 explicitly before 1.4.75 (this guide's advice; the entry does not specify each earlier default). +- basic-auth (REASONED; docs): Load mod_auth and mod_authn_file, select the htpasswd file backend and require valid-user Basic authentication on / over TLS; application login is preferable. +- password-file (REASONED; docs, authn-source, htpasswd-tool, debian-package, fedora-package): Create the user file with Apache htpasswd (Debian apache2-utils; Fedora httpd-tools is this guide's package assessment); lighttpd 1.4.75 reads username:stored-hash entries and checks {SHA} SHA-1 and $apr1$ internally, with other hashes of at least 13 characters checked through crypt() when built with crypt support; under USE_LIB_CRYPTO_MD4, the $1+ntlm$ path derives an NTLM hash and calls crypt() with that derived value and a $1$ salt. Check the build and platform's accepted hashes before choosing a flag; this check is this guide's compatibility advice. +- mfa (REASONED; docs, authelia, cloudflare-access): Basic authentication is single-factor; add MFA by fronting this service with Cloudflare Access or an MFA-capable proxy (this guide's requirement). Authelia v4.39.28's complete supported-proxy list has no lighttpd entry (this guide's inference from the table); Cloudflare Access documents self-hosted application and MFA policy controls; applying them to lighttpd is this guide's assessment. This guide assesses that a lighttpd origin reachable without passing through Access is not protected by it. +- verify-config (REASONED; docs, lighttpd-verify-source, systemd-reload): lighttpd 1.4.75 -tt -f /etc/lighttpd/lighttpd.conf tests the named file for syntax errors and initializes modules; configuration failures prevent the && reload (this guide's inference from the nonzero process result and shell chaining). The cited upstream unit starts from that file and defines a USR1 reload through ExecReload; systemctl reload triggers the service action, whose signal does not establish completion. Confirm the installed unit uses that file and defines reload (this guide's advice). +- verify-redirect (REASONED; docs): The HTTP header request should show a redirect to HTTPS; these are read-and-judge status checks. +- verify-auth (REASONED; docs): An HTTPS request without credentials should return 401 once authentication is enabled. +- verify-listeners (REASONED; tls): Read every listener with ss -tlnp: front-door authentication does not establish that no other listener serves content. The ss version is unrecorded. +- verify-host (REASONED; docs, lighttpd-verify-source, apache-vhosts): In lighttpd 1.4.75, request-time $HTTP["host"] matching uses the HTTP Host value, while TLS SNI can select mod_openssl settings before request headers; a Host outside a conditional auth.require falls back to global configuration for that rule (this guide's inference from conditional merging and mod_auth documentation). On a protected path, this guide interprets 401 as the pass and 200 as the protected path served without authentication on this TLS connection to a client that sends another host. Apache 2.4.69 uses the SNI hostname, when the client sends it and the server supports SNI, for name-based TLS virtual-host selection; varying Host while keeping URL and SNI fixed is this guide's auth-check advice. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "aa010c3e401699d0b9ff9541720750e57529d097494238fca31e5db6a0c3826f", + "components": { + "tls": { + "name": "lighttpd TLS minimum", + "basis": "1.4.56", + "sources": { + "sbff3cb2e01a8": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_SSL" + } + }, + "key": { + "name": "lighttpd separate-key minimum", + "basis": "1.4.53", + "sources": { + "sbff3cb2e01a8": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_SSL" + } + }, + "redirect": { + "name": "lighttpd redirect default transition", + "basis": "1.4.75", + "sources": { + "s6de629323c64": "https://redmine.lighttpd.net/projects/lighttpd/wiki/HowToRedirectHttpToHttps" + } + }, + "modules-source": { + "name": "lighttpd module loader source", + "basis": "1.4.75", + "sources": { + "s8680e561bed2": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L395-L544", + "s8e9688979427": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L270-L290", + "s9bd716d27e8d": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L382-L393", + "s21288573041a": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L662-L945", + "s67bb3733d81c": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L1684-L1711" + } + }, + "authn-source": { + "name": "lighttpd file authentication source", + "basis": "1.4.75", + "sources": { + "s2c11c0fbc0ca": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_authn_file.c#L340-L397", + "se971effb489f": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_authn_file.c#L613-L740" + } + }, + "lighttpd-verify-source": { + "name": "lighttpd verify source", + "basis": "1.4.75", + "sources": { + "s0bbb4c861d5e": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/lighttpd.8#L22-L23", + "sca076bcd18dc": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/lighttpd.8#L33-L37", + "s9e69235fbbc7": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1439-L1451", + "s87104d328493": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1466-L1467", + "sba72bab46867": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1510-L1523", + "s2faa9b3149ac": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1942-L1953", + "s87b4b266bb0e": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L2320-L2384", + "sa989ef425f1d": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L229-L235", + "sf9d56b052c61": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/systemd/lighttpd.service#L5-L11", + "s6a29b290d83a": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/request.c#L423-L430", + "s2fc4cf82abd9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/request.c#L1235-L1247", + "s3f692a48bd01": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile-glue.c#L504-L508", + "sbb09703388c3": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L213-L223", + "s8bb6bd7774f9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_openssl.c#L1025-L1034", + "sb4c43fa1a807": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_openssl.c#L1294-L1330", + "s9e49b928cc81": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/response.c#L227-L228" + } + }, + "docs": { + "name": "lighttpd documentation", + "basis": "unknown", + "sources": { + "s1dbd1ce27da5": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Mod_auth", + "s6d5666b862a0": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ConfigurationOptions", + "s6de629323c64": "https://redmine.lighttpd.net/projects/lighttpd/wiki/HowToRedirectHttpToHttps" + } + }, + "htpasswd-tool": { + "name": "Apache htpasswd manual", + "basis": "2.4", + "sources": { + "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" + } + }, + "debian-package": { + "name": "Debian apache2-utils package", + "basis": "trixie", + "sources": { + "s436bdf5f1fb9": "https://packages.debian.org/trixie/apache2-utils" + } + }, + "authelia": { + "name": "Authelia proxy support", + "basis": "v4.39.28", + "sources": { + "s77083bc8faa2": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L24-L36" + } + }, + "cloudflare-access": { + "name": "Cloudflare Access documentation", + "basis": "commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "sfddb92e438ca": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L14-L16", + "s7cafe1b17afd": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54", + "s603bdb6b24ab": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L45-L45", + "s863803bd9a1f": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L48-L48", + "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" + } + }, + "fedora-package": { + "name": "Fedora httpd-tools package", + "basis": "unknown", + "sources": { + "s919088d03a5e": "https://packages.fedoraproject.org/pkgs/httpd/httpd-tools/" + } + }, + "apache-vhosts": { + "name": "Apache virtual-host source", + "basis": "2.4.69", + "sources": { + "s558db45d4d61": "https://github.com/apache/httpd/blob/2.4.69/docs/manual/vhosts/details.xml#L217-L231" + } + }, + "systemd-reload": { + "name": "systemd reload manual", + "basis": "v262", + "sources": { + "sb3f5069cd250": "https://github.com/systemd/systemd/blob/v262/man/systemctl.xml#L462-L474", + "s441757b7048f": "https://github.com/systemd/systemd/blob/v262/man/systemd.service.xml#L498-L519" + } + }, + "release-news": { + "name": "lighttpd release NEWS", + "basis": "1.4.75", + "sources": { + "s4e990a9e5ec1": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1660-L1660", + "se97c93bf64a8": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1927-L1927", + "s1658d22c71f9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L2465-L2465", + "s95dd525fe4ac": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L2505-L2505", + "sc0d0162991d5": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L6-L7" + } + } + }, + "claims": { + "tls-defaults": {"text": "For this guide's 1.4.56-and-later scope, NEWS records the mod_openssl MinProtocol TLSv1.2 default in 1.4.56. Disabling SSLv2, SSLv3, TLS 1.0 and TLS 1.1 is this guide's reading of that floor; NEWS does not establish defaults for every later release.", "components": ["tls", "release-news"], "sources": ["tls:sbff3cb2e01a8", "release-news:s4e990a9e5ec1", "release-news:se97c93bf64a8"], "status": "REASONED"}, + "tls-listeners": {"text": "Load mod_openssl and enable TLS on both :443 and [::]:443 with ssl.pemfile and ssl.privkey; omitting the IPv6 socket leaves IPv6 clients on plain HTTP.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED"}, + "certificate-chain": {"text": "ssl.pemfile uses fullchain.pem rather than a certificate without its chain.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED"}, + "separate-key": {"text": "NEWS records optional mod_openssl ssl.privkey in 1.4.53. On older versions, concatenate certificate and key into ssl.pemfile (this guide's compatibility advice; the entry does not establish that procedure).", "components": ["key", "release-news"], "sources": ["key:sbff3cb2e01a8", "release-news:s1658d22c71f9", "release-news:s95dd525fe4ac"], "status": "REASONED"}, + "protocol-floor": {"text": "Set MinProtocol to TLSv1.2 with ssl.openssl.ssl-conf-cmd explicitly (this guide's advice); NEWS records that default for mod_openssl in 1.4.56, not every later release.", "components": ["tls", "release-news"], "sources": ["tls:sbff3cb2e01a8", "release-news:s4e990a9e5ec1", "release-news:se97c93bf64a8"], "status": "REASONED"}, + "module-defaults": {"text": "In lighttpd 1.4.75, mod_h2 is inserted unless server.feature-flags disables server.h2proto (enabled by default); when server.compat-module-load is enabled (the default), mod_indexfile and mod_dirlisting are conditionally added when enabled, mod_staticfile is added when absent, and compatibility handling can also add other modules, including authentication and TLS modules under their respective conditions. Load mod_redirect explicitly.", "components": ["docs", "modules-source"], "sources": ["docs:s6d5666b862a0", "modules-source:s8680e561bed2", "modules-source:s8e9688979427", "modules-source:s9bd716d27e8d", "modules-source:s21288573041a", "modules-source:s67bb3733d81c"], "status": "REASONED"}, + "http-redirect": {"text": "For the http scheme, redirect to HTTPS preserving authority, path and query; port 80 must redirect rather than serve content.", "components": ["docs"], "sources": ["docs:s6de629323c64"], "status": "REASONED"}, + "redirect-code": {"text": "NEWS records url.redirect-code = 308 as the new default in 1.4.75. Set 308 explicitly before 1.4.75 (this guide's advice; the entry does not specify each earlier default).", "components": ["redirect", "release-news"], "sources": ["redirect:s6de629323c64", "release-news:sc0d0162991d5"], "status": "REASONED"}, + "basic-auth": {"text": "Load mod_auth and mod_authn_file, select the htpasswd file backend and require valid-user Basic authentication on / over TLS; application login is preferable.", "components": ["docs"], "sources": ["docs:s1dbd1ce27da5"], "status": "REASONED"}, + "password-file": {"text": "Create the user file with Apache htpasswd (Debian apache2-utils; Fedora httpd-tools is this guide's package assessment); lighttpd 1.4.75 reads username:stored-hash entries and checks {SHA} SHA-1 and $apr1$ internally, with other hashes of at least 13 characters checked through crypt() when built with crypt support; under USE_LIB_CRYPTO_MD4, the $1+ntlm$ path derives an NTLM hash and calls crypt() with that derived value and a $1$ salt. Check the build and platform's accepted hashes before choosing a flag; this check is this guide's compatibility advice.", "components": ["docs", "authn-source", "htpasswd-tool", "debian-package", "fedora-package"], "sources": ["docs:s1dbd1ce27da5", "authn-source:s2c11c0fbc0ca", "authn-source:se971effb489f", "htpasswd-tool:scde9004bb967", "debian-package:s436bdf5f1fb9", "fedora-package:s919088d03a5e"], "status": "REASONED"}, + "mfa": {"text": "Basic authentication is single-factor; add MFA by fronting this service with Cloudflare Access or an MFA-capable proxy (this guide's requirement). Authelia v4.39.28's complete supported-proxy list has no lighttpd entry (this guide's inference from the table); Cloudflare Access documents self-hosted application and MFA policy controls; applying them to lighttpd is this guide's assessment. This guide assesses that a lighttpd origin reachable without passing through Access is not protected by it.", "components": ["docs", "authelia", "cloudflare-access"], "sources": ["docs:s1dbd1ce27da5", "authelia:s77083bc8faa2", "cloudflare-access:sfddb92e438ca", "cloudflare-access:s7cafe1b17afd", "cloudflare-access:sdcac732541da", "cloudflare-access:s603bdb6b24ab", "cloudflare-access:s863803bd9a1f"], "status": "REASONED"}, + "verify-config": {"text": "lighttpd 1.4.75 -tt -f /etc/lighttpd/lighttpd.conf tests the named file for syntax errors and initializes modules; configuration failures prevent the && reload (this guide's inference from the nonzero process result and shell chaining). The cited upstream unit starts from that file and defines a USR1 reload through ExecReload; systemctl reload triggers the service action, whose signal does not establish completion. Confirm the installed unit uses that file and defines reload (this guide's advice).", "components": ["docs", "lighttpd-verify-source", "systemd-reload"], "sources": ["docs:s6d5666b862a0", "lighttpd-verify-source:s0bbb4c861d5e", "lighttpd-verify-source:sca076bcd18dc", "lighttpd-verify-source:s9e69235fbbc7", "lighttpd-verify-source:s87104d328493", "lighttpd-verify-source:sba72bab46867", "lighttpd-verify-source:s2faa9b3149ac", "lighttpd-verify-source:s87b4b266bb0e", "lighttpd-verify-source:sa989ef425f1d", "lighttpd-verify-source:sf9d56b052c61", "systemd-reload:sb3f5069cd250", "systemd-reload:s441757b7048f"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "The HTTP header request should show a redirect to HTTPS; these are read-and-judge status checks.", "components": ["docs"], "sources": ["docs:s6de629323c64"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "An HTTPS request without credentials should return 401 once authentication is enabled.", "components": ["docs"], "sources": ["docs:s1dbd1ce27da5"], "status": "REASONED", "verify": [1]}, + "verify-listeners": {"text": "Read every listener with ss -tlnp: front-door authentication does not establish that no other listener serves content. The ss version is unrecorded.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED", "verify": [1]}, + "verify-host": {"text": "In lighttpd 1.4.75, request-time $HTTP[\"host\"] matching uses the HTTP Host value, while TLS SNI can select mod_openssl settings before request headers; a Host outside a conditional auth.require falls back to global configuration for that rule (this guide's inference from conditional merging and mod_auth documentation). On a protected path, this guide interprets 401 as the pass and 200 as the protected path served without authentication on this TLS connection to a client that sends another host. Apache 2.4.69 uses the SNI hostname, when the client sends it and the server supports SNI, for name-based TLS virtual-host selection; varying Host while keeping URL and SNI fixed is this guide's auth-check advice.", "components": ["docs", "lighttpd-verify-source", "apache-vhosts"], "sources": ["docs:s1dbd1ce27da5", "lighttpd-verify-source:s6a29b290d83a", "lighttpd-verify-source:s2fc4cf82abd9", "lighttpd-verify-source:s3f692a48bd01", "lighttpd-verify-source:sbb09703388c3", "lighttpd-verify-source:s8bb6bd7774f9", "lighttpd-verify-source:sb4c43fa1a807", "lighttpd-verify-source:s9e49b928cc81", "apache-vhosts:s558db45d4d61"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/nginx.md b/plugin/skills/secure-deployment-config/references/nginx.md index fb4e47790..73db0191b 100644 --- a/plugin/skills/secure-deployment-config/references/nginx.md +++ b/plugin/skills/secure-deployment-config/references/nginx.md @@ -1,199 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-17", - "documentation_checked": "2026-09", - "body_sha256": "07eda7c75b5cada2d56a730e2c7a33df34730eec5139f55d0545aad278723811", - "components": { - "ng": { - "name": "nginx documentation", - "basis": "unknown", - "sources": { - "s6a0728ca7609": "https://nginx.org/en/docs/http/configuring_https_servers.html", - "s40bdf1af1596": "https://nginx.org/en/docs/http/ngx_http_core_module.html", - "sb3bee9429629": "https://nginx.org/en/docs/http/ngx_http_limit_req_module.html", - "sddc43aac8779": "https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html", - "sf3430c5a0b22": "https://nginx.org/en/docs/http/ngx_http_proxy_module.html", - "sf32ce2956917": "https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html", - "sddacc1191f5f": "https://nginx.org/en/docs/http/ngx_http_realip_module.html" - } - }, - "policy": { - "name": "Mozilla TLS policy", - "basis": "unknown", - "sources": { - "s529b0eabe2ed": "https://ssl-config.mozilla.org/" - } - }, - "certbot": { - "name": "Certbot nginx configurator", - "basis": "v2.3.0", - "sources": { - "s3a02bfe1d7ad": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-nginx/certbot_nginx/_internal/configurator.py#L711" - } - }, - "hsts": { - "name": "HSTS specification", - "basis": "RFC 6797", - "sources": { - "s3f5af5f440f9": "https://www.rfc-editor.org/rfc/rfc6797.html#section-6.1.2" - } - }, - "nginx-source": { - "name": "nginx source", - "basis": "release-1.26.3", - "sources": { - "s0ae4e9bc8d07": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L216-L218", - "s002242c2d0cd": "https://github.com/nginx/nginx/blob/release-1.26.3/docs/xml/nginx/changes.xml#L470-L480", - "s305b7194043c": "https://github.com/nginx/nginx/blob/release-1.26.3/docs/xml/nginx/changes.xml#L739-L775", - "sbea253fd242b": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/v2/ngx_http_v2_module.c#L76-L81", - "s936193a42402": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/ngx_http_core_module.c#L4181-L4188", - "sac3cbbf7fc48": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L857-L862", - "s66c86979411e": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L153-L172", - "sabb195c173d5": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L788-L802", - "sa930199b563a": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L74-L80", - "seb1f8a9d91f9": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L666-L687", - "sf6eac8f6749d": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L788-L796", - "s3df6e64cd6a3": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_request_module.c#L135-L182", - "sced570e33c0f": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/ngx_http_request.h#L78-L138", - "sfafe27857e33": "https://github.com/nginx/nginx/blob/release-1.26.3/src/event/ngx_event_openssl.c#L4568-L4576" - } - }, - "authelia": { - "name": "Authelia nginx integration", - "basis": "v4.39.28", - "sources": { - "s279cd6449b53": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L39-L45", - "sdf01968ab996": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L245-L247", - "s237ae14bbb4a": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L517-L519" - } - }, - "oauth2-proxy": { - "name": "oauth2-proxy nginx integration", - "basis": "v7.15.4", - "sources": { - "sc62400f0ea8a": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/integrations/nginx.md#L16-L18" - } - }, - "cloudflare-access": { - "name": "Cloudflare Access web applications", - "basis": "unknown", - "sources": { - "sfe56578bedf4": "https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/" - } - }, - "httpd": { - "name": "Apache httpd source", - "basis": "2.4.69", - "sources": { - "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128", - "sf711558bdf3c": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L45", - "s1c1a101a70e7": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L290-L295", - "s4482c55d79a2": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L382" - } - }, - "debian-apache2": { - "name": "Debian apache2 packaging", - "basis": "2.4.69-1", - "sources": { - "sc2a26b3a7af5": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/control#L92-101", - "s62f6ed2866ab": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1" - } - }, - "owasp": { - "name": "OWASP Password Storage Cheat Sheet", - "basis": "commit 72865b1e", - "sources": { - "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" - } - } - }, - "claims": { - "certbot": {"text": "The guide says certbot --nginx edits the server block; certificate acquisition is delegated to the linked certificate guides.", "components": ["ng", "certbot"], "sources": ["ng:s6a0728ca7609", "certbot:s3a02bfe1d7ad"], "status": "REASONED"}, - "tls": {"text": "443 IPv4/IPv6 TLS uses certificate chain and private key, TLSv1.2/TLSv1.3 and ssl_prefer_server_ciphers off.", "components": ["ng"], "sources": ["ng:s6a0728ca7609"], "status": "REASONED"}, - "http2": {"text": "At nginx release-1.26.3, http2 on is the HTTP/2 module directive; nginx 1.25.1 introduced it and deprecated the older listen 443 ssl http2 parameter.", "components": ["ng", "nginx-source"], "sources": ["ng:s40bdf1af1596", "nginx-source:s002242c2d0cd", "nginx-source:sbea253fd242b", "nginx-source:s936193a42402"], "status": "REASONED"}, - "tokens": {"text": "server_tokens off suppresses nginx version in headers and error pages.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, - "hsts": {"text": "Send HSTS only after HTTPS works; includeSubDomains commits every subdomain for max-age.", "components": ["ng", "hsts", "nginx-source"], "sources": ["ng:s6a0728ca7609", "hsts:s3f5af5f440f9", "nginx-source:s0ae4e9bc8d07"], "status": "REASONED"}, - "hsts-inheritance": {"text": "At nginx release-1.26.3, a location add_header replaces inherited server headers; always changes response-code coverage, not inheritance.", "components": ["ng", "nginx-source"], "sources": ["ng:s40bdf1af1596", "nginx-source:s0ae4e9bc8d07", "nginx-source:seb1f8a9d91f9", "nginx-source:sf6eac8f6749d"], "status": "REASONED"}, - "proxy": {"text": "Proxy to loopback 127.0.0.1:3000 and set Host, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, - "forwarded-host": {"text": "Overwrite X-Forwarded-Host: client-set values otherwise pass upstream.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, - "cipher-policy": {"text": "Use Mozilla for explicit cipher policy; retain the stated protocol floor.", "components": ["ng", "policy"], "sources": ["ng:s6a0728ca7609", "policy:s529b0eabe2ed"], "status": "REASONED"}, - "tickets": {"text": "At nginx release-1.26.3, ssl_session_tickets defaults on; nginx 1.23.2 introduced automatic ticket-key rotation with a shared ssl_session_cache.", "components": ["ng", "nginx-source"], "sources": ["ng:s6a0728ca7609", "nginx-source:sac3cbbf7fc48", "nginx-source:s305b7194043c", "nginx-source:sfafe27857e33"], "status": "REASONED"}, - "redirect": {"text": "Port 80 on IPv4/IPv6 redirects with 301; a default server reflecting $host can redirect unmatched hosts off-site, so use 444 or a canonical host.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, - "password-file": {"text": "At httpd 2.4.69, htpasswd -B selects bcrypt, bare -B uses cost 5, and -C sets the cost; the OWASP cheat sheet at commit 72865b1e recommends at least 10 for legacy bcrypt. Debian apache2 2.4.69-1 packages htpasswd in apache2-utils.", "components": ["ng", "httpd", "owasp", "debian-apache2"], "sources": ["ng:sf32ce2956917", "debian-apache2:sc2a26b3a7af5", "debian-apache2:s62f6ed2866ab", "httpd:s309ecdd34379", "httpd:sf711558bdf3c", "httpd:s1c1a101a70e7", "httpd:s4482c55d79a2", "owasp:s4531e65b6502"], "status": "REASONED"}, - "basic": {"text": "auth_basic and auth_basic_user_file gate the existing proxied location over TLS; preserve its forwarded headers.", "components": ["ng"], "sources": ["ng:sf32ce2956917", "ng:sf3430c5a0b22"], "status": "REASONED"}, - "mtls": {"text": "At nginx release-1.26.3, ssl_client_certificate and ssl_verify_client on configure trusted client-certificate verification.", "components": ["ng", "nginx-source"], "sources": ["ng:s6a0728ca7609", "nginx-source:s66c86979411e", "nginx-source:sabb195c173d5", "nginx-source:sa930199b563a"], "status": "REASONED"}, - "mfa": {"text": "At nginx release-1.26.3, auth_request permits 2xx authentication subresponses and returns 401/403; Authelia v4.39.28 and oauth2-proxy v7.15.4 document nginx auth_request integration, and Cloudflare Access documents protecting self-hosted web applications; the nginx source cited here does not cover those integrations. MFA is this guide's requirement; whether a given installation enforces it depends on its configured portal or Access policy (this guide's assessment).", "components": ["ng", "nginx-source", "authelia", "oauth2-proxy", "cloudflare-access"], "sources": ["ng:sf32ce2956917", "nginx-source:s3df6e64cd6a3", "nginx-source:sced570e33c0f", "authelia:s279cd6449b53", "authelia:sdf01968ab996", "authelia:s237ae14bbb4a", "oauth2-proxy:sc62400f0ea8a", "cloudflare-access:sfe56578bedf4"], "status": "REASONED"}, - "body-limit": {"text": "client_max_body_size defaults to 1m; 10m rejects larger bodies with 413, and 0 disables the check.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, - "rate-limit": {"text": "Per-address limit_req uses a 10r/s zone, burst 20 and nodelay; rejection status defaults to 503.", "components": ["ng"], "sources": ["ng:sb3bee9429629"], "status": "REASONED"}, - "connection-limit": {"text": "limit_conn apiconn 10 caps concurrent connections per client address; it can also return 503.", "components": ["ng"], "sources": ["ng:sddc43aac8779"], "status": "REASONED"}, - "location-auth": {"text": "Keep limits in the authenticated location; sibling locations do not inherit its auth or proxy headers. Repeat auth or inherit it from server; auth_basic off is deliberate.", "components": ["ng"], "sources": ["ng:sf32ce2956917", "ng:s40bdf1af1596", "ng:sf3430c5a0b22"], "status": "REASONED"}, - "read-timeout": {"text": "proxy_read_timeout 60s limits gaps between upstream reads, not total response duration; active streams can continue indefinitely.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, - "client-address": {"text": "At a direct edge trust the appended last X-Forwarded-For entry; behind a CDN, recover the client with trusted set_real_ip_from and real_ip_header before per-client limits.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22", "ng:sddacc1191f5f", "ng:sb3bee9429629", "ng:sddc43aac8779"], "status": "REASONED"}, - "verify-config": {"text": "Run nginx -t before reload; no configuration-test outcome is recorded.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, - "verify-redirect": {"text": "HTTP should return 301 with an HTTPS Location.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, - "verify-tls": {"text": "HTTPS must validate without -k; TLS 1.1-only rejection must be protocol_version, not a generic/local failure; test TLS 1.0 separately.", "components": ["ng"], "sources": ["ng:s6a0728ca7609"], "status": "REASONED", "verify": [1]}, - "verify-auth": {"text": "An open path may return 200; the protected path must reject no credentials with 401/403, and valid credentials reach the app rather than 401.", "components": ["ng"], "sources": ["ng:sf32ce2956917"], "status": "REASONED", "verify": [1]}, - "verify-size": {"text": "Authenticated 9M succeeds normally only after raising the 1m default; 11M returns 413. Disable the check with 0 in isolation to distinguish backend refusal.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, - "verify-rate": {"text": "Concurrent unauthenticated probes exercise pre-auth limits; 503 does not identify the limiter. Compare both off and each alone with measured arrival rate.", "components": ["ng"], "sources": ["ng:sb3bee9429629", "ng:sddc43aac8779", "ng:sf32ce2956917"], "status": "REASONED", "verify": [1]}, - "verify-backend": {"text": "Inspect every listener: direct public access to backend 3000 bypasses proxy TLS/auth even when front-door probes pass.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED", "verify": [1]} - } -} ---- # nginx: TLS and authentication - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-17; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +Written against nginx source release-1.26.3, Certbot nginx configurator v2.3.0, Authelia nginx integration v4.39.28, oauth2-proxy nginx integration v7.15.4, Apache httpd source 2.4.69, Debian apache2 packaging 2.4.69-1, HSTS specification RFC 6797, and OWASP Password Storage Cheat Sheet commit 72865b1e; 3 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 28 claims, all reasoned from cited sources. -Components (recorded basis): - -- ng: nginx documentation (unknown) -- policy: Mozilla TLS policy (unknown) -- certbot: Certbot nginx configurator (v2.3.0) -- hsts: HSTS specification (RFC 6797) -- nginx-source: nginx source (release-1.26.3) -- authelia: Authelia nginx integration (v4.39.28) -- oauth2-proxy: oauth2-proxy nginx integration (v7.15.4) -- cloudflare-access: Cloudflare Access web applications (unknown) -- httpd: Apache httpd source (2.4.69) -- debian-apache2: Debian apache2 packaging (2.4.69-1) -- owasp: OWASP Password Storage Cheat Sheet (commit 72865b1e) - -Claims (full source mapping in the front matter): - -- certbot (REASONED; ng, certbot): The guide says certbot --nginx edits the server block; certificate acquisition is delegated to the linked certificate guides. -- tls (REASONED; ng): 443 IPv4/IPv6 TLS uses certificate chain and private key, TLSv1.2/TLSv1.3 and ssl_prefer_server_ciphers off. -- http2 (REASONED; ng, nginx-source): At nginx release-1.26.3, http2 on is the HTTP/2 module directive; nginx 1.25.1 introduced it and deprecated the older listen 443 ssl http2 parameter. -- tokens (REASONED; ng): server_tokens off suppresses nginx version in headers and error pages. -- hsts (REASONED; ng, hsts, nginx-source): Send HSTS only after HTTPS works; includeSubDomains commits every subdomain for max-age. -- hsts-inheritance (REASONED; ng, nginx-source): At nginx release-1.26.3, a location add_header replaces inherited server headers; always changes response-code coverage, not inheritance. -- proxy (REASONED; ng): Proxy to loopback 127.0.0.1:3000 and set Host, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host. -- forwarded-host (REASONED; ng): Overwrite X-Forwarded-Host: client-set values otherwise pass upstream. -- cipher-policy (REASONED; ng, policy): Use Mozilla for explicit cipher policy; retain the stated protocol floor. -- tickets (REASONED; ng, nginx-source): At nginx release-1.26.3, ssl_session_tickets defaults on; nginx 1.23.2 introduced automatic ticket-key rotation with a shared ssl_session_cache. -- redirect (REASONED; ng): Port 80 on IPv4/IPv6 redirects with 301; a default server reflecting $host can redirect unmatched hosts off-site, so use 444 or a canonical host. -- password-file (REASONED; ng, httpd, owasp, debian-apache2): At httpd 2.4.69, htpasswd -B selects bcrypt, bare -B uses cost 5, and -C sets the cost; the OWASP cheat sheet at commit 72865b1e recommends at least 10 for legacy bcrypt. Debian apache2 2.4.69-1 packages htpasswd in apache2-utils. -- basic (REASONED; ng): auth_basic and auth_basic_user_file gate the existing proxied location over TLS; preserve its forwarded headers. -- mtls (REASONED; ng, nginx-source): At nginx release-1.26.3, ssl_client_certificate and ssl_verify_client on configure trusted client-certificate verification. -- mfa (REASONED; ng, nginx-source, authelia, oauth2-proxy, cloudflare-access): At nginx release-1.26.3, auth_request permits 2xx authentication subresponses and returns 401/403; Authelia v4.39.28 and oauth2-proxy v7.15.4 document nginx auth_request integration, and Cloudflare Access documents protecting self-hosted web applications; the nginx source cited here does not cover those integrations. MFA is this guide's requirement; whether a given installation enforces it depends on its configured portal or Access policy (this guide's assessment). -- body-limit (REASONED; ng): client_max_body_size defaults to 1m; 10m rejects larger bodies with 413, and 0 disables the check. -- rate-limit (REASONED; ng): Per-address limit_req uses a 10r/s zone, burst 20 and nodelay; rejection status defaults to 503. -- connection-limit (REASONED; ng): limit_conn apiconn 10 caps concurrent connections per client address; it can also return 503. -- location-auth (REASONED; ng): Keep limits in the authenticated location; sibling locations do not inherit its auth or proxy headers. Repeat auth or inherit it from server; auth_basic off is deliberate. -- read-timeout (REASONED; ng): proxy_read_timeout 60s limits gaps between upstream reads, not total response duration; active streams can continue indefinitely. -- client-address (REASONED; ng): At a direct edge trust the appended last X-Forwarded-For entry; behind a CDN, recover the client with trusted set_real_ip_from and real_ip_header before per-client limits. -- verify-config (REASONED; ng): Run nginx -t before reload; no configuration-test outcome is recorded. -- verify-redirect (REASONED; ng): HTTP should return 301 with an HTTPS Location. -- verify-tls (REASONED; ng): HTTPS must validate without -k; TLS 1.1-only rejection must be protocol_version, not a generic/local failure; test TLS 1.0 separately. -- verify-auth (REASONED; ng): An open path may return 200; the protected path must reject no credentials with 401/403, and valid credentials reach the app rather than 401. -- verify-size (REASONED; ng): Authenticated 9M succeeds normally only after raising the 1m default; 11M returns 413. Disable the check with 0 in isolation to distinguish backend refusal. -- verify-rate (REASONED; ng): Concurrent unauthenticated probes exercise pre-auth limits; 503 does not identify the limiter. Compare both off and each alone with measured arrival rate. -- verify-backend (REASONED; ng): Inspect every listener: direct public access to backend 3000 bypasses proxy TLS/auth even when front-door probes pass. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Get a certificate first: [free-certificates.md](free-certificates.md) for a public host (note that `certbot --nginx` edits the server block for you), or [self-signed.md](self-signed.md) for internal use. @@ -425,3 +234,220 @@ ss -tlnp # read every listener; 3000: the app itself: 127.0.0.1 only, never 0. - Debian apache2 2.4.69-1 `apache2-utils` package description listing htpasswd (checked October 2026): https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/control#L92-101 - Debian apache2 packaging version 2.4.69-1 (checked October 2026): https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1 - OWASP Password Storage Cheat Sheet at commit 72865b1e (checked October 2026): https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/nginx.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- nginx-source: nginx source (release-1.26.3) +- ng: nginx documentation (unknown) +- certbot: Certbot nginx configurator (v2.3.0) +- authelia: Authelia nginx integration (v4.39.28) +- oauth2-proxy: oauth2-proxy nginx integration (v7.15.4) +- cloudflare-access: Cloudflare Access web applications (unknown) +- httpd: Apache httpd source (2.4.69) +- debian-apache2: Debian apache2 packaging (2.4.69-1) +- policy: Mozilla TLS policy (unknown) +- hsts: HSTS specification (RFC 6797) +- owasp: OWASP Password Storage Cheat Sheet (commit 72865b1e) + +Claims (full source mapping in the JSON block below): + +- certbot (REASONED; ng, certbot): The guide says certbot --nginx edits the server block; certificate acquisition is delegated to the linked certificate guides. +- tls (REASONED; ng): 443 IPv4/IPv6 TLS uses certificate chain and private key, TLSv1.2/TLSv1.3 and ssl_prefer_server_ciphers off. +- http2 (REASONED; ng, nginx-source): At nginx release-1.26.3, http2 on is the HTTP/2 module directive; nginx 1.25.1 introduced it and deprecated the older listen 443 ssl http2 parameter. +- tokens (REASONED; ng): server_tokens off suppresses nginx version in headers and error pages. +- hsts (REASONED; ng, hsts, nginx-source): Send HSTS only after HTTPS works; includeSubDomains commits every subdomain for max-age. +- hsts-inheritance (REASONED; ng, nginx-source): At nginx release-1.26.3, a location add_header replaces inherited server headers; always changes response-code coverage, not inheritance. +- proxy (REASONED; ng): Proxy to loopback 127.0.0.1:3000 and set Host, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host. +- forwarded-host (REASONED; ng): Overwrite X-Forwarded-Host: client-set values otherwise pass upstream. +- cipher-policy (REASONED; ng, policy): Use Mozilla for explicit cipher policy; retain the stated protocol floor. +- tickets (REASONED; ng, nginx-source): At nginx release-1.26.3, ssl_session_tickets defaults on; nginx 1.23.2 introduced automatic ticket-key rotation with a shared ssl_session_cache. +- redirect (REASONED; ng): Port 80 on IPv4/IPv6 redirects with 301; a default server reflecting $host can redirect unmatched hosts off-site, so use 444 or a canonical host. +- password-file (REASONED; ng, httpd, owasp, debian-apache2): At httpd 2.4.69, htpasswd -B selects bcrypt, bare -B uses cost 5, and -C sets the cost; the OWASP cheat sheet at commit 72865b1e recommends at least 10 for legacy bcrypt. Debian apache2 2.4.69-1 packages htpasswd in apache2-utils. +- basic (REASONED; ng): auth_basic and auth_basic_user_file gate the existing proxied location over TLS; preserve its forwarded headers. +- mtls (REASONED; ng, nginx-source): At nginx release-1.26.3, ssl_client_certificate and ssl_verify_client on configure trusted client-certificate verification. +- mfa (REASONED; ng, nginx-source, authelia, oauth2-proxy, cloudflare-access): At nginx release-1.26.3, auth_request permits 2xx authentication subresponses and returns 401/403; Authelia v4.39.28 and oauth2-proxy v7.15.4 document nginx auth_request integration, and Cloudflare Access documents protecting self-hosted web applications; the nginx source cited here does not cover those integrations. MFA is this guide's requirement; whether a given installation enforces it depends on its configured portal or Access policy (this guide's assessment). +- body-limit (REASONED; ng): client_max_body_size defaults to 1m; 10m rejects larger bodies with 413, and 0 disables the check. +- rate-limit (REASONED; ng): Per-address limit_req uses a 10r/s zone, burst 20 and nodelay; rejection status defaults to 503. +- connection-limit (REASONED; ng): limit_conn apiconn 10 caps concurrent connections per client address; it can also return 503. +- location-auth (REASONED; ng): Keep limits in the authenticated location; sibling locations do not inherit its auth or proxy headers. Repeat auth or inherit it from server; auth_basic off is deliberate. +- read-timeout (REASONED; ng): proxy_read_timeout 60s limits gaps between upstream reads, not total response duration; active streams can continue indefinitely. +- client-address (REASONED; ng): At a direct edge trust the appended last X-Forwarded-For entry; behind a CDN, recover the client with trusted set_real_ip_from and real_ip_header before per-client limits. +- verify-config (REASONED; ng): Run nginx -t before reload; no configuration-test outcome is recorded. +- verify-redirect (REASONED; ng): HTTP should return 301 with an HTTPS Location. +- verify-tls (REASONED; ng): HTTPS must validate without -k; TLS 1.1-only rejection must be protocol_version, not a generic/local failure; test TLS 1.0 separately. +- verify-auth (REASONED; ng): An open path may return 200; the protected path must reject no credentials with 401/403, and valid credentials reach the app rather than 401. +- verify-size (REASONED; ng): Authenticated 9M succeeds normally only after raising the 1m default; 11M returns 413. Disable the check with 0 in isolation to distinguish backend refusal. +- verify-rate (REASONED; ng): Concurrent unauthenticated probes exercise pre-auth limits; 503 does not identify the limiter. Compare both off and each alone with measured arrival rate. +- verify-backend (REASONED; ng): Inspect every listener: direct public access to backend 3000 bypasses proxy TLS/auth even when front-door probes pass. + +````json +{ + "schema": 1, + "checked": "2026-10-17", + "documentation_checked": "2026-09", + "body_sha256": "07eda7c75b5cada2d56a730e2c7a33df34730eec5139f55d0545aad278723811", + "components": { + "nginx-source": { + "name": "nginx source", + "basis": "release-1.26.3", + "sources": { + "s0ae4e9bc8d07": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L216-L218", + "s002242c2d0cd": "https://github.com/nginx/nginx/blob/release-1.26.3/docs/xml/nginx/changes.xml#L470-L480", + "s305b7194043c": "https://github.com/nginx/nginx/blob/release-1.26.3/docs/xml/nginx/changes.xml#L739-L775", + "sbea253fd242b": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/v2/ngx_http_v2_module.c#L76-L81", + "s936193a42402": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/ngx_http_core_module.c#L4181-L4188", + "sac3cbbf7fc48": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L857-L862", + "s66c86979411e": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L153-L172", + "sabb195c173d5": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L788-L802", + "sa930199b563a": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L74-L80", + "seb1f8a9d91f9": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L666-L687", + "sf6eac8f6749d": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L788-L796", + "s3df6e64cd6a3": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_request_module.c#L135-L182", + "sced570e33c0f": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/ngx_http_request.h#L78-L138", + "sfafe27857e33": "https://github.com/nginx/nginx/blob/release-1.26.3/src/event/ngx_event_openssl.c#L4568-L4576" + } + }, + "ng": { + "name": "nginx documentation", + "basis": "unknown", + "sources": { + "s6a0728ca7609": "https://nginx.org/en/docs/http/configuring_https_servers.html", + "s40bdf1af1596": "https://nginx.org/en/docs/http/ngx_http_core_module.html", + "sb3bee9429629": "https://nginx.org/en/docs/http/ngx_http_limit_req_module.html", + "sddc43aac8779": "https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html", + "sf3430c5a0b22": "https://nginx.org/en/docs/http/ngx_http_proxy_module.html", + "sf32ce2956917": "https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html", + "sddacc1191f5f": "https://nginx.org/en/docs/http/ngx_http_realip_module.html" + } + }, + "certbot": { + "name": "Certbot nginx configurator", + "basis": "v2.3.0", + "sources": { + "s3a02bfe1d7ad": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-nginx/certbot_nginx/_internal/configurator.py#L711" + } + }, + "authelia": { + "name": "Authelia nginx integration", + "basis": "v4.39.28", + "sources": { + "s279cd6449b53": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L39-L45", + "sdf01968ab996": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L245-L247", + "s237ae14bbb4a": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L517-L519" + } + }, + "oauth2-proxy": { + "name": "oauth2-proxy nginx integration", + "basis": "v7.15.4", + "sources": { + "sc62400f0ea8a": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/integrations/nginx.md#L16-L18" + } + }, + "cloudflare-access": { + "name": "Cloudflare Access web applications", + "basis": "unknown", + "sources": { + "sfe56578bedf4": "https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/" + } + }, + "httpd": { + "name": "Apache httpd source", + "basis": "2.4.69", + "sources": { + "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128", + "sf711558bdf3c": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L45", + "s1c1a101a70e7": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L290-L295", + "s4482c55d79a2": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L382" + } + }, + "debian-apache2": { + "name": "Debian apache2 packaging", + "basis": "2.4.69-1", + "sources": { + "sc2a26b3a7af5": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/control#L92-101", + "s62f6ed2866ab": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1" + } + }, + "policy": { + "name": "Mozilla TLS policy", + "basis": "unknown", + "sources": { + "s529b0eabe2ed": "https://ssl-config.mozilla.org/" + } + }, + "hsts": { + "name": "HSTS specification", + "basis": "RFC 6797", + "sources": { + "s3f5af5f440f9": "https://www.rfc-editor.org/rfc/rfc6797.html#section-6.1.2" + } + }, + "owasp": { + "name": "OWASP Password Storage Cheat Sheet", + "basis": "commit 72865b1e", + "sources": { + "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" + } + } + }, + "claims": { + "certbot": {"text": "The guide says certbot --nginx edits the server block; certificate acquisition is delegated to the linked certificate guides.", "components": ["ng", "certbot"], "sources": ["ng:s6a0728ca7609", "certbot:s3a02bfe1d7ad"], "status": "REASONED"}, + "tls": {"text": "443 IPv4/IPv6 TLS uses certificate chain and private key, TLSv1.2/TLSv1.3 and ssl_prefer_server_ciphers off.", "components": ["ng"], "sources": ["ng:s6a0728ca7609"], "status": "REASONED"}, + "http2": {"text": "At nginx release-1.26.3, http2 on is the HTTP/2 module directive; nginx 1.25.1 introduced it and deprecated the older listen 443 ssl http2 parameter.", "components": ["ng", "nginx-source"], "sources": ["ng:s40bdf1af1596", "nginx-source:s002242c2d0cd", "nginx-source:sbea253fd242b", "nginx-source:s936193a42402"], "status": "REASONED"}, + "tokens": {"text": "server_tokens off suppresses nginx version in headers and error pages.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, + "hsts": {"text": "Send HSTS only after HTTPS works; includeSubDomains commits every subdomain for max-age.", "components": ["ng", "hsts", "nginx-source"], "sources": ["ng:s6a0728ca7609", "hsts:s3f5af5f440f9", "nginx-source:s0ae4e9bc8d07"], "status": "REASONED"}, + "hsts-inheritance": {"text": "At nginx release-1.26.3, a location add_header replaces inherited server headers; always changes response-code coverage, not inheritance.", "components": ["ng", "nginx-source"], "sources": ["ng:s40bdf1af1596", "nginx-source:s0ae4e9bc8d07", "nginx-source:seb1f8a9d91f9", "nginx-source:sf6eac8f6749d"], "status": "REASONED"}, + "proxy": {"text": "Proxy to loopback 127.0.0.1:3000 and set Host, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, + "forwarded-host": {"text": "Overwrite X-Forwarded-Host: client-set values otherwise pass upstream.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, + "cipher-policy": {"text": "Use Mozilla for explicit cipher policy; retain the stated protocol floor.", "components": ["ng", "policy"], "sources": ["ng:s6a0728ca7609", "policy:s529b0eabe2ed"], "status": "REASONED"}, + "tickets": {"text": "At nginx release-1.26.3, ssl_session_tickets defaults on; nginx 1.23.2 introduced automatic ticket-key rotation with a shared ssl_session_cache.", "components": ["ng", "nginx-source"], "sources": ["ng:s6a0728ca7609", "nginx-source:sac3cbbf7fc48", "nginx-source:s305b7194043c", "nginx-source:sfafe27857e33"], "status": "REASONED"}, + "redirect": {"text": "Port 80 on IPv4/IPv6 redirects with 301; a default server reflecting $host can redirect unmatched hosts off-site, so use 444 or a canonical host.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, + "password-file": {"text": "At httpd 2.4.69, htpasswd -B selects bcrypt, bare -B uses cost 5, and -C sets the cost; the OWASP cheat sheet at commit 72865b1e recommends at least 10 for legacy bcrypt. Debian apache2 2.4.69-1 packages htpasswd in apache2-utils.", "components": ["ng", "httpd", "owasp", "debian-apache2"], "sources": ["ng:sf32ce2956917", "debian-apache2:sc2a26b3a7af5", "debian-apache2:s62f6ed2866ab", "httpd:s309ecdd34379", "httpd:sf711558bdf3c", "httpd:s1c1a101a70e7", "httpd:s4482c55d79a2", "owasp:s4531e65b6502"], "status": "REASONED"}, + "basic": {"text": "auth_basic and auth_basic_user_file gate the existing proxied location over TLS; preserve its forwarded headers.", "components": ["ng"], "sources": ["ng:sf32ce2956917", "ng:sf3430c5a0b22"], "status": "REASONED"}, + "mtls": {"text": "At nginx release-1.26.3, ssl_client_certificate and ssl_verify_client on configure trusted client-certificate verification.", "components": ["ng", "nginx-source"], "sources": ["ng:s6a0728ca7609", "nginx-source:s66c86979411e", "nginx-source:sabb195c173d5", "nginx-source:sa930199b563a"], "status": "REASONED"}, + "mfa": {"text": "At nginx release-1.26.3, auth_request permits 2xx authentication subresponses and returns 401/403; Authelia v4.39.28 and oauth2-proxy v7.15.4 document nginx auth_request integration, and Cloudflare Access documents protecting self-hosted web applications; the nginx source cited here does not cover those integrations. MFA is this guide's requirement; whether a given installation enforces it depends on its configured portal or Access policy (this guide's assessment).", "components": ["ng", "nginx-source", "authelia", "oauth2-proxy", "cloudflare-access"], "sources": ["ng:sf32ce2956917", "nginx-source:s3df6e64cd6a3", "nginx-source:sced570e33c0f", "authelia:s279cd6449b53", "authelia:sdf01968ab996", "authelia:s237ae14bbb4a", "oauth2-proxy:sc62400f0ea8a", "cloudflare-access:sfe56578bedf4"], "status": "REASONED"}, + "body-limit": {"text": "client_max_body_size defaults to 1m; 10m rejects larger bodies with 413, and 0 disables the check.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, + "rate-limit": {"text": "Per-address limit_req uses a 10r/s zone, burst 20 and nodelay; rejection status defaults to 503.", "components": ["ng"], "sources": ["ng:sb3bee9429629"], "status": "REASONED"}, + "connection-limit": {"text": "limit_conn apiconn 10 caps concurrent connections per client address; it can also return 503.", "components": ["ng"], "sources": ["ng:sddc43aac8779"], "status": "REASONED"}, + "location-auth": {"text": "Keep limits in the authenticated location; sibling locations do not inherit its auth or proxy headers. Repeat auth or inherit it from server; auth_basic off is deliberate.", "components": ["ng"], "sources": ["ng:sf32ce2956917", "ng:s40bdf1af1596", "ng:sf3430c5a0b22"], "status": "REASONED"}, + "read-timeout": {"text": "proxy_read_timeout 60s limits gaps between upstream reads, not total response duration; active streams can continue indefinitely.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, + "client-address": {"text": "At a direct edge trust the appended last X-Forwarded-For entry; behind a CDN, recover the client with trusted set_real_ip_from and real_ip_header before per-client limits.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22", "ng:sddacc1191f5f", "ng:sb3bee9429629", "ng:sddc43aac8779"], "status": "REASONED"}, + "verify-config": {"text": "Run nginx -t before reload; no configuration-test outcome is recorded.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP should return 301 with an HTTPS Location.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, + "verify-tls": {"text": "HTTPS must validate without -k; TLS 1.1-only rejection must be protocol_version, not a generic/local failure; test TLS 1.0 separately.", "components": ["ng"], "sources": ["ng:s6a0728ca7609"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "An open path may return 200; the protected path must reject no credentials with 401/403, and valid credentials reach the app rather than 401.", "components": ["ng"], "sources": ["ng:sf32ce2956917"], "status": "REASONED", "verify": [1]}, + "verify-size": {"text": "Authenticated 9M succeeds normally only after raising the 1m default; 11M returns 413. Disable the check with 0 in isolation to distinguish backend refusal.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, + "verify-rate": {"text": "Concurrent unauthenticated probes exercise pre-auth limits; 503 does not identify the limiter. Compare both off and each alone with measured arrival rate.", "components": ["ng"], "sources": ["ng:sb3bee9429629", "ng:sddc43aac8779", "ng:sf32ce2956917"], "status": "REASONED", "verify": [1]}, + "verify-backend": {"text": "Inspect every listener: direct public access to backend 3000 bypasses proxy TLS/auth even when front-door probes pass.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/nomad-consul.md b/plugin/skills/secure-deployment-config/references/nomad-consul.md index dd71a0200..dd8f15aa5 100644 --- a/plugin/skills/secure-deployment-config/references/nomad-consul.md +++ b/plugin/skills/secure-deployment-config/references/nomad-consul.md @@ -1,149 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-26", - "documentation_checked": "2026-09", - "body_sha256": "05ae6961d3225ec0e28ef043428dfeea38f86a7be4cfd93f6a6d58b4990f084a", - "components": { - "nomad": { - "name": "Nomad Community Edition", - "basis": "2.0.7", - "sources": { - "s52d80d2821ed": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/agent/config.go", - "sa1c2951d3ca6": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/config/ui.go", - "s553448c7957c": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/auth/auth.go", - "s7496bcf129dc": "https://github.com/hashicorp/nomad/blob/v2.0.7/acl/acl.go", - "sd665f15bc452": "https://github.com/hashicorp/nomad/blob/v2.0.7/drivers/exec/driver.go", - "sd5c86223bdf5": "https://github.com/hashicorp/nomad/blob/v2.0.7/client/config/config.go", - "sd5f2c389c6ae": "https://github.com/hashicorp/nomad/blob/v2.0.7/drivers/rawexec/driver.go", - "se7ba8e4262dc": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/config/tls.go", - "sc3e36e830b67": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/acl_endpoint.go", - "s754c582b7565": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/agent/agent.go", - "s8193264e48c2": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/acl.go", - "sba39c1a9119a": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/meta.go#L96", - "sa04bf8c95cb6": "https://github.com/hashicorp/nomad/blob/v2.0.7/api/api.go#L388-L389" - } - }, - "consul": { - "name": "Consul Community Edition", - "basis": "2.0.4", - "sources": { - "s679a306e1b08": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/config/default.go", - "s8a0b5abac641": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/config/builder.go", - "sc4fb30ca3702": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/consul/acl.go", - "s35b783d75794": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/acl_endpoint.go", - "sa77598ec3695": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/consul/acl_endpoint.go", - "s00748de44382": "https://github.com/hashicorp/consul/blob/v2.0.4/acl/acl.go", - "sab8a5866d9c9": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/http.go", - "sfeb50129fc10": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/token/persistence.go" - } - } - }, - "claims": { - "nomad-bind": {"text": "Nomad defaults bind_addr=0.0.0.0 and all three listeners fall back to it; setting only addresses.http leaves RPC and gossip on bind_addr.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, - "nomad-http": {"text": "Nomad HTTP API/UI uses 4646; UI defaults enabled at /ui/.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:sa1c2951d3ca6"], "status": "REASONED"}, - "nomad-rpc": {"text": "Nomad RPC uses 4647; keep it on private or management addresses.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, - "nomad-gossip": {"text": "Nomad Serf uses 4648 TCP/UDP; gossip is unencrypted until encrypt is configured.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, - "nomad-dev": {"text": "Linux -dev binds 127.0.0.1 except -dev-connect, which binds 0.0.0.0. Wildcard behavior was not observed.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, - "nomad-acl-default": {"text": "ACLs default disabled; enable acl.enabled on every agent before exposing the API.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s553448c7957c", "nomad:s7496bcf129dc"], "status": "REASONED"}, - "nomad-debug": {"text": "Debug endpoints still require enable_debug with ACLs off; -dev enables it.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s7496bcf129dc"], "status": "REASONED"}, - "nomad-exec": {"text": "On a root Linux client, exec uses a chroot and task user, default nobody, with root refused by the default exec denylist; no client job was run.", "components": ["nomad"], "sources": ["nomad:sd665f15bc452", "nomad:sd5c86223bdf5"], "status": "REASONED"}, - "nomad-raw-exec": {"text": "raw_exec has no isolation and defaults off, but -dev enables it.", "components": ["nomad"], "sources": ["nomad:sd5f2c389c6ae", "nomad:s52d80d2821ed"], "status": "REASONED"}, - "nomad-remote-exec": {"text": "Remote task execution defaults enabled through disable_remote_exec=false.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, - "nomad-tls": {"text": "HTTP/RPC TLS defaults off; enable both tls.http and tls.rpc.", "components": ["nomad"], "sources": ["nomad:se7ba8e4262dc"], "status": "REASONED"}, - "nomad-bootstrap-reset": {"text": "Unauthenticated PUT /v1/acl/bootstrap claims the first management token after ACL enablement; repeat is blocked until the operator reset file in data_dir/server.", "components": ["nomad"], "sources": ["nomad:sc3e36e830b67", "nomad:s754c582b7565"], "status": "REASONED"}, - "consul-client-bind": {"text": "Consul client_addr defaults 127.0.0.1; widening it or addresses.http makes the API reachable.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-http": {"text": "Consul HTTP API is 8500; UI defaults off and -dev enables /ui/ on that port.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-dns": {"text": "Consul DNS uses 8600 TCP/UDP on client_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-rpc": {"text": "Consul server RPC uses 8300 on bind_addr, default 0.0.0.0.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-lan": {"text": "Consul LAN Serf uses 8301 TCP/UDP on bind_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-wan": {"text": "Consul WAN Serf uses 8302 TCP/UDP on bind_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-https": {"text": "Consul HTTPS defaults disabled.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-grpc": {"text": "Plaintext gRPC defaults disabled; servers open TLS gRPC on 8503 on client_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-acl-default": {"text": "Consul ACLs default off and disabled ACL resolution grants management rights; with ACLs enabled default_policy still defaults allow.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641", "consul:sc4fb30ca3702"], "status": "REASONED"}, - "consul-bootstrap-reset": {"text": "Bootstrap is unauthenticated and one-time until an operator writes acl-bootstrap-reset; bootstrap privately before exposing the API.", "components": ["consul"], "sources": ["consul:s35b783d75794", "consul:sa77598ec3695"], "status": "REASONED"}, - "consul-script": {"text": "enable_script_checks defaults off; enabling without ACLs or allow_write_http_from only warns. Prefer enable_local_script_checks if needed.", "components": ["consul"], "sources": ["consul:s8a0b5abac641"], "status": "REASONED"}, - "consul-remote-exec": {"text": "consul exec defaults disabled through disable_remote_exec=true.", "components": ["consul"], "sources": ["consul:s679a306e1b08"], "status": "REASONED"}, - "consul-tls-gossip": {"text": "TLS verification defaults off and gossip is unencrypted without encrypt.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, - "verify-inventory": {"text": "Inventory TCP/UDP on private or management addresses; wildcard exposed binds and actual external isolation remain reasoned.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [1]}, - "loopback-listeners": {"text": "Loopback runs used substitute TCP/Serf UDP ports; Consul DNS, WAN and gRPC were disabled, so their live listeners were not demonstrated.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "DEMONSTRATED", "evidence": "On the loopback run, the agents listened on TCP on test ports standing in for Nomad's 4646, 4647 and 4648 and Consul's 8500, 8300 and 8301 (the runs turned off Consul's DNS, Serf WAN and gRPC), and on UDP on the two Serf ports."}, - "nomad-acl-probe": {"text": "Loopback anonymous self/jobs/agent probes distinguished ACLs-off from ACLs-on; self remains 200 but changes AccessorID, while jobs and agent become 403.", "components": ["nomad"], "sources": ["nomad:s553448c7957c", "nomad:s7496bcf129dc", "nomad:s8193264e48c2"], "status": "DEMONSTRATED", "evidence": "All of these outcomes were observed with the curl probe block and these commands, on agents listening on test ports.", "verify": [2]}, - "nomad-agent-acl": {"text": "Server-handled paths can conceal a client with ACLs off; local /v1/agent/self exposed that client with 200 while the server returned 403.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s7496bcf129dc"], "status": "DEMONSTRATED", "evidence": "In a cluster whose server had ACLs on and was bootstrapped, a client agent configured with `acl { enabled = false }` answered the first two paths with the ACLs-on results and `/v1/agent/self` with `200`, while the server answered it with `403`.", "verify": [2]}, - "nomad-bootstrap-run": {"text": "ACL probe rejection did not prove bootstrap complete; first bootstrap issued management credentials and the second returned already-done.", "components": ["nomad"], "sources": ["nomad:sc3e36e830b67"], "status": "DEMONSTRATED", "evidence": "Before bootstrap, the probe already printed the ACLs-on results; the first `nomad acl bootstrap` printed a management token and the second failed with `Unexpected response code: 400 (ACL bootstrap already done ...)`."}, - "nomad-anonymous-policy": {"text": "Anonymous rights come from the named anonymous policy; historical exported-token inspection found it absent, then present with submit-job despite unchanged probe results.", "components": ["nomad"], "sources": ["nomad:s8193264e48c2"], "status": "DEMONSTRATED", "evidence": "After an `anonymous` policy granting only `submit-job` was applied, the probe's results did not change, an anonymous job registration returned `200`, and `nomad acl policy info anonymous`, run in the same earlier form, showed the policy."}, - "nomad-job-registration": {"text": "Anonymous registration succeeded with ACLs off and failed with ACLs on and no anonymous policy; registration is not a demonstrated job execution.", "components": ["nomad"], "sources": ["nomad:s553448c7957c", "nomad:s7496bcf129dc"], "status": "DEMONSTRATED", "evidence": "With ACLs off, an anonymous job registration also returned `200`; with ACLs on and no `anonymous` policy, it returned `403`."}, - "nomad-token-input": {"text": "CLI accepts -token or NOMAD_TOKEN, not stdin; the current prompted one-command prefix avoids argv/history but retains same-user/root environment visibility. That form has no recorded live Nomad run.", "components": ["nomad"], "sources": ["nomad:sba39c1a9119a", "nomad:sa04bf8c95cb6"], "status": "REASONED", "verify": [3]}, - "consul-acl-probe": {"text": "Loopback ACLs-off gave self 401/agent 200; enabled allow gave self 403/agent 200; enabled deny gave agent 403 with anonymous-permission text.", "components": ["consul"], "sources": ["consul:sc4fb30ca3702", "consul:s8a0b5abac641"], "status": "DEMONSTRATED", "evidence": "All of these outcomes were observed with this block and these commands, on agents listening on test ports.", "verify": [4]}, - "consul-agent-acl": {"text": "Each agent applies its own enabled/default_policy; mixed server/client configurations gave different results on their own APIs.", "components": ["consul"], "sources": ["consul:s8a0b5abac641", "consul:sc4fb30ca3702"], "status": "DEMONSTRATED", "evidence": "with the server on the `allow` policy and the client on `deny`, it printed `200` on `/v1/agent/self` against the server and the deny results against the client.", "verify": [4]}, - "consul-bootstrap-run": {"text": "Deny results appeared before bootstrap; first bootstrap issued a token and the second refused with bootstrap no longer allowed.", "components": ["consul"], "sources": ["consul:s35b783d75794", "consul:sa77598ec3695"], "status": "DEMONSTRATED", "evidence": "the first `consul acl bootstrap` printed a token and the second failed with `Unexpected response code: 403 (Permission denied: ACL bootstrap no longer allowed ...)`"}, - "consul-anonymous-token": {"text": "Inspect anonymous accessor 00000000-0000-0000-0000-000000000002 for policies, roles and identities; its recorded read listed none.", "components": ["consul"], "sources": ["consul:s00748de44382"], "status": "DEMONSTRATED", "evidence": "the anonymous token's read listed none of those sections."}, - "consul-default-token": {"text": "An agent default token substitutes for anonymous requests; recorded key-write default token yielded self 200 and agent 403 yet allowed an unauthenticated KV write.", "components": ["consul"], "sources": ["consul:sab8a5866d9c9", "consul:sfeb50129fc10"], "status": "DEMONSTRATED", "evidence": "On the loopback run, a default token holding key write, set through the agent's token API on a deny-policy agent, made the block print `200` on `/v1/acl/token/self` and a `403` on `/v1/agent/self` without the anonymous-token message, while a key-value write without a token succeeded.", "verify": [4]}, - "consul-kv": {"text": "Anonymous KV writes succeeded with ACLs off and allow policy, and were refused under deny.", "components": ["consul"], "sources": ["consul:sc4fb30ca3702", "consul:s679a306e1b08"], "status": "DEMONSTRATED", "evidence": "On the same runs an anonymous key-value write succeeded in both exposed states and was refused with `403` with the deny policy"}, - "consul-script-run": {"text": "With default script-check settings, remote script registration was refused.", "components": ["consul"], "sources": ["consul:s8a0b5abac641"], "status": "DEMONSTRATED", "evidence": "with script checks at their default, registering a script check through the API was refused with \"Scripts are disabled on this agent from remote calls\"."}, - "verify-tcp": {"text": "Probe each actual public IP with a known-open control; connected establishes reachability, refused/timeout only local nonreachability. This guide cross-references another guide's run; UDP is not tested.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [5]}, - "verify-address-limits": {"text": "TCP guard refuses zero/dotted-IPv6 forms but permits loopback and some hex forms reaching the probing host; use public addresses and corroborate surprising connections with host sockets.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [5]} - } -} ---- # Nomad and Consul: the scheduler and service-mesh APIs - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +Written against Nomad Community Edition 2.0.7 and Consul Community Edition 2.0.4. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 42 claims: 13 demonstrated by a check that ran and 29 reasoned from cited sources. -Metadata reviewed 2026-09-26; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- nomad: Nomad Community Edition (2.0.7) -- consul: Consul Community Edition (2.0.4) - -Claims (full source mapping in the front matter): - -- nomad-bind (REASONED; nomad): Nomad defaults bind_addr=0.0.0.0 and all three listeners fall back to it; setting only addresses.http leaves RPC and gossip on bind_addr. -- nomad-http (REASONED; nomad): Nomad HTTP API/UI uses 4646; UI defaults enabled at /ui/. -- nomad-rpc (REASONED; nomad): Nomad RPC uses 4647; keep it on private or management addresses. -- nomad-gossip (REASONED; nomad): Nomad Serf uses 4648 TCP/UDP; gossip is unencrypted until encrypt is configured. -- nomad-dev (REASONED; nomad): Linux -dev binds 127.0.0.1 except -dev-connect, which binds 0.0.0.0. Wildcard behavior was not observed. -- nomad-acl-default (REASONED; nomad): ACLs default disabled; enable acl.enabled on every agent before exposing the API. -- nomad-debug (REASONED; nomad): Debug endpoints still require enable_debug with ACLs off; -dev enables it. -- nomad-exec (REASONED; nomad): On a root Linux client, exec uses a chroot and task user, default nobody, with root refused by the default exec denylist; no client job was run. -- nomad-raw-exec (REASONED; nomad): raw_exec has no isolation and defaults off, but -dev enables it. -- nomad-remote-exec (REASONED; nomad): Remote task execution defaults enabled through disable_remote_exec=false. -- nomad-tls (REASONED; nomad): HTTP/RPC TLS defaults off; enable both tls.http and tls.rpc. -- nomad-bootstrap-reset (REASONED; nomad): Unauthenticated PUT /v1/acl/bootstrap claims the first management token after ACL enablement; repeat is blocked until the operator reset file in data_dir/server. -- consul-client-bind (REASONED; consul): Consul client_addr defaults 127.0.0.1; widening it or addresses.http makes the API reachable. -- consul-http (REASONED; consul): Consul HTTP API is 8500; UI defaults off and -dev enables /ui/ on that port. -- consul-dns (REASONED; consul): Consul DNS uses 8600 TCP/UDP on client_addr. -- consul-rpc (REASONED; consul): Consul server RPC uses 8300 on bind_addr, default 0.0.0.0. -- consul-lan (REASONED; consul): Consul LAN Serf uses 8301 TCP/UDP on bind_addr. -- consul-wan (REASONED; consul): Consul WAN Serf uses 8302 TCP/UDP on bind_addr. -- consul-https (REASONED; consul): Consul HTTPS defaults disabled. -- consul-grpc (REASONED; consul): Plaintext gRPC defaults disabled; servers open TLS gRPC on 8503 on client_addr. -- consul-acl-default (REASONED; consul): Consul ACLs default off and disabled ACL resolution grants management rights; with ACLs enabled default_policy still defaults allow. -- consul-bootstrap-reset (REASONED; consul): Bootstrap is unauthenticated and one-time until an operator writes acl-bootstrap-reset; bootstrap privately before exposing the API. -- consul-script (REASONED; consul): enable_script_checks defaults off; enabling without ACLs or allow_write_http_from only warns. Prefer enable_local_script_checks if needed. -- consul-remote-exec (REASONED; consul): consul exec defaults disabled through disable_remote_exec=true. -- consul-tls-gossip (REASONED; consul): TLS verification defaults off and gossip is unencrypted without encrypt. -- verify-inventory (REASONED; nomad, consul): Inventory TCP/UDP on private or management addresses; wildcard exposed binds and actual external isolation remain reasoned. -- loopback-listeners (DEMONSTRATED; nomad, consul): Loopback runs used substitute TCP/Serf UDP ports; Consul DNS, WAN and gRPC were disabled, so their live listeners were not demonstrated. -- nomad-acl-probe (DEMONSTRATED; nomad): Loopback anonymous self/jobs/agent probes distinguished ACLs-off from ACLs-on; self remains 200 but changes AccessorID, while jobs and agent become 403. -- nomad-agent-acl (DEMONSTRATED; nomad): Server-handled paths can conceal a client with ACLs off; local /v1/agent/self exposed that client with 200 while the server returned 403. -- nomad-bootstrap-run (DEMONSTRATED; nomad): ACL probe rejection did not prove bootstrap complete; first bootstrap issued management credentials and the second returned already-done. -- nomad-anonymous-policy (DEMONSTRATED; nomad): Anonymous rights come from the named anonymous policy; historical exported-token inspection found it absent, then present with submit-job despite unchanged probe results. -- nomad-job-registration (DEMONSTRATED; nomad): Anonymous registration succeeded with ACLs off and failed with ACLs on and no anonymous policy; registration is not a demonstrated job execution. -- nomad-token-input (REASONED; nomad): CLI accepts -token or NOMAD_TOKEN, not stdin; the current prompted one-command prefix avoids argv/history but retains same-user/root environment visibility. That form has no recorded live Nomad run. -- consul-acl-probe (DEMONSTRATED; consul): Loopback ACLs-off gave self 401/agent 200; enabled allow gave self 403/agent 200; enabled deny gave agent 403 with anonymous-permission text. -- consul-agent-acl (DEMONSTRATED; consul): Each agent applies its own enabled/default_policy; mixed server/client configurations gave different results on their own APIs. -- consul-bootstrap-run (DEMONSTRATED; consul): Deny results appeared before bootstrap; first bootstrap issued a token and the second refused with bootstrap no longer allowed. -- consul-anonymous-token (DEMONSTRATED; consul): Inspect anonymous accessor 00000000-0000-0000-0000-000000000002 for policies, roles and identities; its recorded read listed none. -- consul-default-token (DEMONSTRATED; consul): An agent default token substitutes for anonymous requests; recorded key-write default token yielded self 200 and agent 403 yet allowed an unauthenticated KV write. -- consul-kv (DEMONSTRATED; consul): Anonymous KV writes succeeded with ACLs off and allow policy, and were refused under deny. -- consul-script-run (DEMONSTRATED; consul): With default script-check settings, remote script registration was refused. -- verify-tcp (REASONED; nomad, consul): Probe each actual public IP with a known-open control; connected establishes reachability, refused/timeout only local nonreachability. This guide cross-references another guide's run; UDP is not tested. -- verify-address-limits (REASONED; nomad, consul): TCP guard refuses zero/dotted-IPv6 forms but permits loopback and some hex forms reaching the probing host; use public addresses and corroborate surprising connections with host sockets. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Nomad schedules and runs workloads, and Consul holds a cluster's service catalog, health checks and key-value store. Both are controlled through an HTTP API that also serves a web UI, and both ship @@ -449,3 +308,170 @@ give the host's public address. A "connected" on a port you did not mean to expo - Consul 2.0.4 default token for requests without one (`acl.tokens.default`, agent token API): https://github.com/hashicorp/consul/blob/v2.0.4/agent/http.go and https://github.com/hashicorp/consul/blob/v2.0.4/agent/token/persistence.go - Nomad 2.0.7 agent debug endpoints when ACLs are disabled (`enable_debug`): https://github.com/hashicorp/nomad/blob/v2.0.7/acl/acl.go - Consul 2.0.4 ACL bootstrap: https://github.com/hashicorp/consul/blob/v2.0.4/agent/acl_endpoint.go and https://github.com/hashicorp/consul/blob/v2.0.4/agent/consul/acl_endpoint.go + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/nomad-consul.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- nomad: Nomad Community Edition (2.0.7) +- consul: Consul Community Edition (2.0.4) + +Claims (full source mapping in the JSON block below): + +- nomad-bind (REASONED; nomad): Nomad defaults bind_addr=0.0.0.0 and all three listeners fall back to it; setting only addresses.http leaves RPC and gossip on bind_addr. +- nomad-http (REASONED; nomad): Nomad HTTP API/UI uses 4646; UI defaults enabled at /ui/. +- nomad-rpc (REASONED; nomad): Nomad RPC uses 4647; keep it on private or management addresses. +- nomad-gossip (REASONED; nomad): Nomad Serf uses 4648 TCP/UDP; gossip is unencrypted until encrypt is configured. +- nomad-dev (REASONED; nomad): Linux -dev binds 127.0.0.1 except -dev-connect, which binds 0.0.0.0. Wildcard behavior was not observed. +- nomad-acl-default (REASONED; nomad): ACLs default disabled; enable acl.enabled on every agent before exposing the API. +- nomad-debug (REASONED; nomad): Debug endpoints still require enable_debug with ACLs off; -dev enables it. +- nomad-exec (REASONED; nomad): On a root Linux client, exec uses a chroot and task user, default nobody, with root refused by the default exec denylist; no client job was run. +- nomad-raw-exec (REASONED; nomad): raw_exec has no isolation and defaults off, but -dev enables it. +- nomad-remote-exec (REASONED; nomad): Remote task execution defaults enabled through disable_remote_exec=false. +- nomad-tls (REASONED; nomad): HTTP/RPC TLS defaults off; enable both tls.http and tls.rpc. +- nomad-bootstrap-reset (REASONED; nomad): Unauthenticated PUT /v1/acl/bootstrap claims the first management token after ACL enablement; repeat is blocked until the operator reset file in data_dir/server. +- consul-client-bind (REASONED; consul): Consul client_addr defaults 127.0.0.1; widening it or addresses.http makes the API reachable. +- consul-http (REASONED; consul): Consul HTTP API is 8500; UI defaults off and -dev enables /ui/ on that port. +- consul-dns (REASONED; consul): Consul DNS uses 8600 TCP/UDP on client_addr. +- consul-rpc (REASONED; consul): Consul server RPC uses 8300 on bind_addr, default 0.0.0.0. +- consul-lan (REASONED; consul): Consul LAN Serf uses 8301 TCP/UDP on bind_addr. +- consul-wan (REASONED; consul): Consul WAN Serf uses 8302 TCP/UDP on bind_addr. +- consul-https (REASONED; consul): Consul HTTPS defaults disabled. +- consul-grpc (REASONED; consul): Plaintext gRPC defaults disabled; servers open TLS gRPC on 8503 on client_addr. +- consul-acl-default (REASONED; consul): Consul ACLs default off and disabled ACL resolution grants management rights; with ACLs enabled default_policy still defaults allow. +- consul-bootstrap-reset (REASONED; consul): Bootstrap is unauthenticated and one-time until an operator writes acl-bootstrap-reset; bootstrap privately before exposing the API. +- consul-script (REASONED; consul): enable_script_checks defaults off; enabling without ACLs or allow_write_http_from only warns. Prefer enable_local_script_checks if needed. +- consul-remote-exec (REASONED; consul): consul exec defaults disabled through disable_remote_exec=true. +- consul-tls-gossip (REASONED; consul): TLS verification defaults off and gossip is unencrypted without encrypt. +- verify-inventory (REASONED; nomad, consul): Inventory TCP/UDP on private or management addresses; wildcard exposed binds and actual external isolation remain reasoned. +- loopback-listeners (DEMONSTRATED; nomad, consul): Loopback runs used substitute TCP/Serf UDP ports; Consul DNS, WAN and gRPC were disabled, so their live listeners were not demonstrated. +- nomad-acl-probe (DEMONSTRATED; nomad): Loopback anonymous self/jobs/agent probes distinguished ACLs-off from ACLs-on; self remains 200 but changes AccessorID, while jobs and agent become 403. +- nomad-agent-acl (DEMONSTRATED; nomad): Server-handled paths can conceal a client with ACLs off; local /v1/agent/self exposed that client with 200 while the server returned 403. +- nomad-bootstrap-run (DEMONSTRATED; nomad): ACL probe rejection did not prove bootstrap complete; first bootstrap issued management credentials and the second returned already-done. +- nomad-anonymous-policy (DEMONSTRATED; nomad): Anonymous rights come from the named anonymous policy; historical exported-token inspection found it absent, then present with submit-job despite unchanged probe results. +- nomad-job-registration (DEMONSTRATED; nomad): Anonymous registration succeeded with ACLs off and failed with ACLs on and no anonymous policy; registration is not a demonstrated job execution. +- nomad-token-input (REASONED; nomad): CLI accepts -token or NOMAD_TOKEN, not stdin; the current prompted one-command prefix avoids argv/history but retains same-user/root environment visibility. That form has no recorded live Nomad run. +- consul-acl-probe (DEMONSTRATED; consul): Loopback ACLs-off gave self 401/agent 200; enabled allow gave self 403/agent 200; enabled deny gave agent 403 with anonymous-permission text. +- consul-agent-acl (DEMONSTRATED; consul): Each agent applies its own enabled/default_policy; mixed server/client configurations gave different results on their own APIs. +- consul-bootstrap-run (DEMONSTRATED; consul): Deny results appeared before bootstrap; first bootstrap issued a token and the second refused with bootstrap no longer allowed. +- consul-anonymous-token (DEMONSTRATED; consul): Inspect anonymous accessor 00000000-0000-0000-0000-000000000002 for policies, roles and identities; its recorded read listed none. +- consul-default-token (DEMONSTRATED; consul): An agent default token substitutes for anonymous requests; recorded key-write default token yielded self 200 and agent 403 yet allowed an unauthenticated KV write. +- consul-kv (DEMONSTRATED; consul): Anonymous KV writes succeeded with ACLs off and allow policy, and were refused under deny. +- consul-script-run (DEMONSTRATED; consul): With default script-check settings, remote script registration was refused. +- verify-tcp (REASONED; nomad, consul): Probe each actual public IP with a known-open control; connected establishes reachability, refused/timeout only local nonreachability. This guide cross-references another guide's run; UDP is not tested. +- verify-address-limits (REASONED; nomad, consul): TCP guard refuses zero/dotted-IPv6 forms but permits loopback and some hex forms reaching the probing host; use public addresses and corroborate surprising connections with host sockets. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "05ae6961d3225ec0e28ef043428dfeea38f86a7be4cfd93f6a6d58b4990f084a", + "components": { + "nomad": { + "name": "Nomad Community Edition", + "basis": "2.0.7", + "sources": { + "s52d80d2821ed": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/agent/config.go", + "sa1c2951d3ca6": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/config/ui.go", + "s553448c7957c": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/auth/auth.go", + "s7496bcf129dc": "https://github.com/hashicorp/nomad/blob/v2.0.7/acl/acl.go", + "sd665f15bc452": "https://github.com/hashicorp/nomad/blob/v2.0.7/drivers/exec/driver.go", + "sd5c86223bdf5": "https://github.com/hashicorp/nomad/blob/v2.0.7/client/config/config.go", + "sd5f2c389c6ae": "https://github.com/hashicorp/nomad/blob/v2.0.7/drivers/rawexec/driver.go", + "se7ba8e4262dc": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/config/tls.go", + "sc3e36e830b67": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/acl_endpoint.go", + "s754c582b7565": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/agent/agent.go", + "s8193264e48c2": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/acl.go", + "sba39c1a9119a": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/meta.go#L96", + "sa04bf8c95cb6": "https://github.com/hashicorp/nomad/blob/v2.0.7/api/api.go#L388-L389" + } + }, + "consul": { + "name": "Consul Community Edition", + "basis": "2.0.4", + "sources": { + "s679a306e1b08": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/config/default.go", + "s8a0b5abac641": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/config/builder.go", + "sc4fb30ca3702": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/consul/acl.go", + "s35b783d75794": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/acl_endpoint.go", + "sa77598ec3695": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/consul/acl_endpoint.go", + "s00748de44382": "https://github.com/hashicorp/consul/blob/v2.0.4/acl/acl.go", + "sab8a5866d9c9": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/http.go", + "sfeb50129fc10": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/token/persistence.go" + } + } + }, + "claims": { + "nomad-bind": {"text": "Nomad defaults bind_addr=0.0.0.0 and all three listeners fall back to it; setting only addresses.http leaves RPC and gossip on bind_addr.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-http": {"text": "Nomad HTTP API/UI uses 4646; UI defaults enabled at /ui/.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:sa1c2951d3ca6"], "status": "REASONED"}, + "nomad-rpc": {"text": "Nomad RPC uses 4647; keep it on private or management addresses.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-gossip": {"text": "Nomad Serf uses 4648 TCP/UDP; gossip is unencrypted until encrypt is configured.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-dev": {"text": "Linux -dev binds 127.0.0.1 except -dev-connect, which binds 0.0.0.0. Wildcard behavior was not observed.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-acl-default": {"text": "ACLs default disabled; enable acl.enabled on every agent before exposing the API.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s553448c7957c", "nomad:s7496bcf129dc"], "status": "REASONED"}, + "nomad-debug": {"text": "Debug endpoints still require enable_debug with ACLs off; -dev enables it.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s7496bcf129dc"], "status": "REASONED"}, + "nomad-exec": {"text": "On a root Linux client, exec uses a chroot and task user, default nobody, with root refused by the default exec denylist; no client job was run.", "components": ["nomad"], "sources": ["nomad:sd665f15bc452", "nomad:sd5c86223bdf5"], "status": "REASONED"}, + "nomad-raw-exec": {"text": "raw_exec has no isolation and defaults off, but -dev enables it.", "components": ["nomad"], "sources": ["nomad:sd5f2c389c6ae", "nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-remote-exec": {"text": "Remote task execution defaults enabled through disable_remote_exec=false.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-tls": {"text": "HTTP/RPC TLS defaults off; enable both tls.http and tls.rpc.", "components": ["nomad"], "sources": ["nomad:se7ba8e4262dc"], "status": "REASONED"}, + "nomad-bootstrap-reset": {"text": "Unauthenticated PUT /v1/acl/bootstrap claims the first management token after ACL enablement; repeat is blocked until the operator reset file in data_dir/server.", "components": ["nomad"], "sources": ["nomad:sc3e36e830b67", "nomad:s754c582b7565"], "status": "REASONED"}, + "consul-client-bind": {"text": "Consul client_addr defaults 127.0.0.1; widening it or addresses.http makes the API reachable.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-http": {"text": "Consul HTTP API is 8500; UI defaults off and -dev enables /ui/ on that port.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-dns": {"text": "Consul DNS uses 8600 TCP/UDP on client_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-rpc": {"text": "Consul server RPC uses 8300 on bind_addr, default 0.0.0.0.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-lan": {"text": "Consul LAN Serf uses 8301 TCP/UDP on bind_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-wan": {"text": "Consul WAN Serf uses 8302 TCP/UDP on bind_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-https": {"text": "Consul HTTPS defaults disabled.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-grpc": {"text": "Plaintext gRPC defaults disabled; servers open TLS gRPC on 8503 on client_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-acl-default": {"text": "Consul ACLs default off and disabled ACL resolution grants management rights; with ACLs enabled default_policy still defaults allow.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641", "consul:sc4fb30ca3702"], "status": "REASONED"}, + "consul-bootstrap-reset": {"text": "Bootstrap is unauthenticated and one-time until an operator writes acl-bootstrap-reset; bootstrap privately before exposing the API.", "components": ["consul"], "sources": ["consul:s35b783d75794", "consul:sa77598ec3695"], "status": "REASONED"}, + "consul-script": {"text": "enable_script_checks defaults off; enabling without ACLs or allow_write_http_from only warns. Prefer enable_local_script_checks if needed.", "components": ["consul"], "sources": ["consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-remote-exec": {"text": "consul exec defaults disabled through disable_remote_exec=true.", "components": ["consul"], "sources": ["consul:s679a306e1b08"], "status": "REASONED"}, + "consul-tls-gossip": {"text": "TLS verification defaults off and gossip is unencrypted without encrypt.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "verify-inventory": {"text": "Inventory TCP/UDP on private or management addresses; wildcard exposed binds and actual external isolation remain reasoned.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [1]}, + "loopback-listeners": {"text": "Loopback runs used substitute TCP/Serf UDP ports; Consul DNS, WAN and gRPC were disabled, so their live listeners were not demonstrated.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "DEMONSTRATED", "evidence": "On the loopback run, the agents listened on TCP on test ports standing in for Nomad's 4646, 4647 and 4648 and Consul's 8500, 8300 and 8301 (the runs turned off Consul's DNS, Serf WAN and gRPC), and on UDP on the two Serf ports."}, + "nomad-acl-probe": {"text": "Loopback anonymous self/jobs/agent probes distinguished ACLs-off from ACLs-on; self remains 200 but changes AccessorID, while jobs and agent become 403.", "components": ["nomad"], "sources": ["nomad:s553448c7957c", "nomad:s7496bcf129dc", "nomad:s8193264e48c2"], "status": "DEMONSTRATED", "evidence": "All of these outcomes were observed with the curl probe block and these commands, on agents listening on test ports.", "verify": [2]}, + "nomad-agent-acl": {"text": "Server-handled paths can conceal a client with ACLs off; local /v1/agent/self exposed that client with 200 while the server returned 403.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s7496bcf129dc"], "status": "DEMONSTRATED", "evidence": "In a cluster whose server had ACLs on and was bootstrapped, a client agent configured with `acl { enabled = false }` answered the first two paths with the ACLs-on results and `/v1/agent/self` with `200`, while the server answered it with `403`.", "verify": [2]}, + "nomad-bootstrap-run": {"text": "ACL probe rejection did not prove bootstrap complete; first bootstrap issued management credentials and the second returned already-done.", "components": ["nomad"], "sources": ["nomad:sc3e36e830b67"], "status": "DEMONSTRATED", "evidence": "Before bootstrap, the probe already printed the ACLs-on results; the first `nomad acl bootstrap` printed a management token and the second failed with `Unexpected response code: 400 (ACL bootstrap already done ...)`."}, + "nomad-anonymous-policy": {"text": "Anonymous rights come from the named anonymous policy; historical exported-token inspection found it absent, then present with submit-job despite unchanged probe results.", "components": ["nomad"], "sources": ["nomad:s8193264e48c2"], "status": "DEMONSTRATED", "evidence": "After an `anonymous` policy granting only `submit-job` was applied, the probe's results did not change, an anonymous job registration returned `200`, and `nomad acl policy info anonymous`, run in the same earlier form, showed the policy."}, + "nomad-job-registration": {"text": "Anonymous registration succeeded with ACLs off and failed with ACLs on and no anonymous policy; registration is not a demonstrated job execution.", "components": ["nomad"], "sources": ["nomad:s553448c7957c", "nomad:s7496bcf129dc"], "status": "DEMONSTRATED", "evidence": "With ACLs off, an anonymous job registration also returned `200`; with ACLs on and no `anonymous` policy, it returned `403`."}, + "nomad-token-input": {"text": "CLI accepts -token or NOMAD_TOKEN, not stdin; the current prompted one-command prefix avoids argv/history but retains same-user/root environment visibility. That form has no recorded live Nomad run.", "components": ["nomad"], "sources": ["nomad:sba39c1a9119a", "nomad:sa04bf8c95cb6"], "status": "REASONED", "verify": [3]}, + "consul-acl-probe": {"text": "Loopback ACLs-off gave self 401/agent 200; enabled allow gave self 403/agent 200; enabled deny gave agent 403 with anonymous-permission text.", "components": ["consul"], "sources": ["consul:sc4fb30ca3702", "consul:s8a0b5abac641"], "status": "DEMONSTRATED", "evidence": "All of these outcomes were observed with this block and these commands, on agents listening on test ports.", "verify": [4]}, + "consul-agent-acl": {"text": "Each agent applies its own enabled/default_policy; mixed server/client configurations gave different results on their own APIs.", "components": ["consul"], "sources": ["consul:s8a0b5abac641", "consul:sc4fb30ca3702"], "status": "DEMONSTRATED", "evidence": "with the server on the `allow` policy and the client on `deny`, it printed `200` on `/v1/agent/self` against the server and the deny results against the client.", "verify": [4]}, + "consul-bootstrap-run": {"text": "Deny results appeared before bootstrap; first bootstrap issued a token and the second refused with bootstrap no longer allowed.", "components": ["consul"], "sources": ["consul:s35b783d75794", "consul:sa77598ec3695"], "status": "DEMONSTRATED", "evidence": "the first `consul acl bootstrap` printed a token and the second failed with `Unexpected response code: 403 (Permission denied: ACL bootstrap no longer allowed ...)`"}, + "consul-anonymous-token": {"text": "Inspect anonymous accessor 00000000-0000-0000-0000-000000000002 for policies, roles and identities; its recorded read listed none.", "components": ["consul"], "sources": ["consul:s00748de44382"], "status": "DEMONSTRATED", "evidence": "the anonymous token's read listed none of those sections."}, + "consul-default-token": {"text": "An agent default token substitutes for anonymous requests; recorded key-write default token yielded self 200 and agent 403 yet allowed an unauthenticated KV write.", "components": ["consul"], "sources": ["consul:sab8a5866d9c9", "consul:sfeb50129fc10"], "status": "DEMONSTRATED", "evidence": "On the loopback run, a default token holding key write, set through the agent's token API on a deny-policy agent, made the block print `200` on `/v1/acl/token/self` and a `403` on `/v1/agent/self` without the anonymous-token message, while a key-value write without a token succeeded.", "verify": [4]}, + "consul-kv": {"text": "Anonymous KV writes succeeded with ACLs off and allow policy, and were refused under deny.", "components": ["consul"], "sources": ["consul:sc4fb30ca3702", "consul:s679a306e1b08"], "status": "DEMONSTRATED", "evidence": "On the same runs an anonymous key-value write succeeded in both exposed states and was refused with `403` with the deny policy"}, + "consul-script-run": {"text": "With default script-check settings, remote script registration was refused.", "components": ["consul"], "sources": ["consul:s8a0b5abac641"], "status": "DEMONSTRATED", "evidence": "with script checks at their default, registering a script check through the API was refused with \"Scripts are disabled on this agent from remote calls\"."}, + "verify-tcp": {"text": "Probe each actual public IP with a known-open control; connected establishes reachability, refused/timeout only local nonreachability. This guide cross-references another guide's run; UDP is not tested.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [5]}, + "verify-address-limits": {"text": "TCP guard refuses zero/dotted-IPv6 forms but permits loopback and some hex forms reaching the probing host; use public addresses and corroborate surprising connections with host sockets.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [5]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/observability-components.md b/plugin/skills/secure-deployment-config/references/observability-components.md index 4ab97e4cd..36fae247d 100644 --- a/plugin/skills/secure-deployment-config/references/observability-components.md +++ b/plugin/skills/secure-deployment-config/references/observability-components.md @@ -1,6 +1,6 @@ # Self-hosted observability components: node_exporter, Alertmanager, Pushgateway, Jaeger, and Loki -Written against node_exporter v1.12.1, Alertmanager v0.34.1, Pushgateway v1.11.3, Jaeger v2.21.0, Loki v3.7.8, Exporter toolkit v0.17.1, OpenTelemetry configtls v1.66.0, and OpenTelemetry basicauth extension v0.160.0, plus 1 more recorded bases; 3 further sources are pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 59 claims: 14 demonstrated by a check that ran and 45 reasoned from cited sources. +Written against node_exporter v1.12.1, Alertmanager v0.34.1, Pushgateway v1.11.3, Jaeger v2.21.0, Loki v3.7.8, Exporter toolkit v0.17.1, OpenTelemetry configtls v1.66.0, and OpenTelemetry basicauth extension v0.160.0, plus 1 more recorded basis; 3 further sources are pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 59 claims: 14 demonstrated by a check that ran and 45 reasoned from cited sources. A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. diff --git a/plugin/skills/secure-deployment-config/references/pocketbase.md b/plugin/skills/secure-deployment-config/references/pocketbase.md index cc6b5fad2..89af0dc7c 100644 --- a/plugin/skills/secure-deployment-config/references/pocketbase.md +++ b/plugin/skills/secure-deployment-config/references/pocketbase.md @@ -1,300 +1,25 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-19", - "documentation_checked": "2026-09", - "body_sha256": "56cf568348e9a9e7890afd9133b526447b70bf593b07da5b93bedb63ce116e85", - "components": { - "pb": { - "name": "PocketBase", - "basis": "v0.40.4", - "sources": { - "saf684a26056b": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/installer.go", - "s4b155ef30262": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/cmd/serve.go", - "sb2129128b86c": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/serve.go", - "s5b66ca69bf6d": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/middlewares.go", - "s04ae861b5709": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/record_query.go", - "s69aa223f52e4": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/record_crud.go", - "sce114368c2ad": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/collection_model_auth_options.go", - "s03dd7dd84c2a": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/settings_model.go", - "seb56d3763223": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/base.go", - "sa5e571f8d473": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/file.go", - "sfd2f779e8e11": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/realtime.go", - "s31a5adccd695": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/backup.go", - "se74188862a55": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/backup_create.go#L36-L60", - "sf6ffdca4dd39": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/middlewares_rate_limit.go", - "s1b04c2cd16ec": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L234-L239", - "sb63f49346abe": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L73-L79", - "sd700e81880f2": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L129-L139", - "s54430c83a38a": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L165-L173", - "s2c8f78f2d5ac": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/tools/osutils/run.go#L10-L19", - "s5b0dd2fd7a6a": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/base.go#L1277-L1286", - "sf7c2fb5e78e7": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/base.go#L1489-L1499" - } - }, - "appwrite": { - "name": "Appwrite", - "basis": "2.2.0", - "sources": { - "s9f5bd3b70f14": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/config/variables.php", - "sadb8c6e7b00a": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/.env", - "s2bb21218c5ee": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/api/account.php", - "s193bfa5c6e22": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/shared/api.php", - "s427bab3733b6": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/general.php", - "se9480e5807b3": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Create.php", - "sf916851a0b10": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php#L82-L84", - "s5b4b3eb2271f": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L129-L134", - "s93647e16b966": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L211-L212", - "sda698c40fc34": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L237-L287", - "s655a0abb9ef0": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php", - "s4ab95ecbf6b5": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/init/constants.php", - "s8e5272615f2e": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/docker-compose.yml", - "s7ba6f9c71972": "https://github.com/appwrite/appwrite/blob/2.2.0/composer.lock#L3461-L3471", - "s5396e119c026": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php#L80-L95", - "s82988de645fa": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php#L103-L120", - "s224cc9d8a387": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Utopia/Database/Documents/User.php#L75-L109", - "sc55465cf878d": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L144-L161", - "sed1bf9453ecb": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L312-L339", - "s8784d02c82c9": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L357-L361", - "sf60a2b283b80": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L503-L507", - "s1fe0e6185518": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php#L221-L227" - } - }, - "pb-docs": { - "name": "PocketBase documentation (rolling)", - "basis": "unknown", - "sources": { - "s9029ab974cf1": "https://pocketbase.io/docs/going-to-production/", - "saedbdfbdfc48": "https://pocketbase.io/docs/api-rules-and-filters/", - "s773e441dd51c": "https://pocketbase.io/docs/authentication/", - "sf711b6345643": "https://pocketbase.io/docs/files-handling/", - "sb850a71fad4d": "https://pocketbase.io/docs/api-realtime/" - } - }, - "appwrite-docs": { - "name": "Appwrite documentation (rolling)", - "basis": "unknown", - "sources": { - "s4a965bdeeb98": "https://appwrite.io/docs/advanced/self-hosting/production/security", - "saa37bb5d4d7e": "https://appwrite.io/docs/partners/project/api-keys", - "sa0bd132f7968": "https://appwrite.io/docs/advanced/security/permissions", - "s58ad28816c5d": "https://appwrite.io/docs/products/databases/tablesdb/legacy/permissions", - "s2bf9a4746d28": "https://appwrite.io/docs/advanced/self-hosting/configuration/environment-variables", - "s000d05106450": "https://appwrite.io/docs/advanced/security/rate-limits", - "s38a1efa0c7d2": "https://appwrite.io/docs/advanced/self-hosting/configuration/email", - "s256ef73f7b85": "https://appwrite.io/docs/advanced/self-hosting/production/backups", - "s221c6aaef8df": "https://appwrite.io/docs/advanced/security/mfa" - } - }, - "curl": { - "name": "curl", - "basis": "curl-8_14_1", - "sources": { - "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", - "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md", - "s82a34ee18632": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/config.md" - } - }, - "utopia-php-database": { - "name": "utopia-php/database", - "basis": "7.3.9", - "sources": { - "s847e0ded0300": "https://github.com/utopia-php/database/blob/54364c687d6ddb3d976d7130b9001c0b29276933/src/Database/Helpers/Permission.php#L151-L155" - } - } - }, - "claims": { - "pb-bootstrap": {"text": "Privately bootstrap through the logged installer URL; superuser create EMAIL PASS exposes passwords in argv. Ordinary registration does not grant superuser rights.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:saf684a26056b"], "status": "REASONED"}, - "pb-installer": {"text": "Installer system-superuser token lasts about 30 minutes; protect its URL and startup logs as credentials.", "components": ["pb"], "sources": ["pb:saf684a26056b"], "status": "REASONED"}, - "pb-ips": {"text": "Superuser IP allowlists are available from v0.38.0; superuserIPs defaults empty and restricts authenticated requests, not /_/ static assets.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s5b66ca69bf6d"], "status": "REASONED"}, - "pb-tls": {"text": "serve with a domain provides native HTTPS/ACME; alternatively terminate TLS at a reverse proxy.", "components": ["pb"], "sources": ["pb:sb2129128b86c"], "status": "REASONED"}, - "pb-bind": {"text": "Without a domain serve defaults to 127.0.0.1:8090; with a domain it uses 0.0.0.0:80/443. Behind a local proxy omit the domain and set --http loopback.", "components": ["pb"], "sources": ["pb:s4b155ef30262"], "status": "REASONED"}, - "pb-origins": {"text": "--origins defaults * and accepts comma-separated allowed origins; CORS does not authorize records.", "components": ["pb"], "sources": ["pb:s4b155ef30262"], "status": "REASONED"}, - "pb-proxy": {"text": "Trust only headers the proxy overwrites, review useLeftmostIP ordering and block direct backend access to protect IP/rate controls.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a"], "status": "REASONED"}, - "pb-dev": {"text": "--dev adds diagnostics including SQL on stderr, not an auth bypass; its configured default comes from New(): true if os.Args[0] starts with a nonempty os.TempDir() or cacheDir() prefix, false otherwise.", "components": ["pb"], "sources": ["pb:seb56d3763223", "pb:s1b04c2cd16ec", "pb:sb63f49346abe", "pb:s2c8f78f2d5ac", "pb:sd700e81880f2", "pb:s5b0dd2fd7a6a", "pb:sf7c2fb5e78e7"], "status": "REASONED"}, - "pb-cli": {"text": "In the pinned CLI wrapper, Start() registers the serve and superuser commands on the root command and executes it.", "components": ["pb"], "sources": ["pb:s54430c83a38a"], "status": "REASONED"}, - "pb-rules": {"text": "Each list/view/create/update/delete rule defaults null (superuser-only); empty opens to guests and nonempty filters. Superusers bypass rules.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:saedbdfbdfc48", "pb:s04ae861b5709"], "status": "REASONED"}, - "pb-manage": {"text": "Top-level manageRule defaults null and permits privileged auth-record changes alongside create/update rules; its validator rejects empty-string rules.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, - "pb-files": {"text": "File fields default unprotected despite locked record rules; Protected plus an authorized viewRule gates downloads, while a public viewRule still permits public access.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sf711b6345643", "pb:sa5e571f8d473"], "status": "REASONED"}, - "pb-file-token": {"text": "Short-lived file tokens provide identity context, not file authorization; viewRule decides. Protect token-bearing file URLs.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sf711b6345643", "pb:sa5e571f8d473"], "status": "REASONED"}, - "pb-realtime": {"text": "Collection subscriptions use listRule and individual-record subscriptions viewRule; SSE/subscription success does not prove event authorization.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sb850a71fad4d", "pb:sfd2f779e8e11"], "status": "REASONED"}, - "pb-password": {"text": "Auth initialization enables password authentication with email as the identity field.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, - "pb-oauth": {"text": "OAuth2 defaults disabled and is unsupported for _superusers.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, - "pb-mfa-default": {"text": "MFA defaults disabled with duration 600 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, - "pb-otp-default": {"text": "OTP defaults disabled with duration 180 seconds and length 8.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, - "pb-alerts": {"text": "Authentication alerts default enabled; retain alerts and provide working email.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, - "pb-auth-token": {"text": "Auth token duration defaults to 432000 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, - "pb-file-duration": {"text": "File token duration defaults to 180 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, - "pb-auth-rule": {"text": "authRule defaults empty; verified-only collections can require verified = true after email verification works.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, - "pb-mfa": {"text": "For superusers retain password auth and enable both OTP and MFA; empty mfa.rule applies to everyone. OTP alone is not a second factor.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, - "pb-logout": {"text": "Stateless authentication means clearing pb.authStore removes only the local copy, not a stolen token.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s773e441dd51c", "pb:s04ae861b5709"], "status": "REASONED"}, - "pb-encryption": {"text": "Settings default to plaintext JSON; select a 32-character environment secret with --encryptionEnv. This encrypts settings, not the whole database or files.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a"], "status": "REASONED"}, - "pb-backups": {"text": "Backups default local with empty cron (automatic backups off); archives contain main and logs databases plus remaining pb_data, excluding the backups directory and tracked exclusions; uploaded collection files on S3 require manual backup. Schedule/retain deliberately and protect archives.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a", "pb:se74188862a55"], "status": "REASONED"}, - "pb-backup-auth": {"text": "Backup downloads require a superuser file token and obey superuser IP restrictions; ordinary auth/file tokens do not suffice.", "components": ["pb"], "sources": ["pb:s31a5adccd695"], "status": "REASONED"}, - "pb-limiter": {"text": "Native limits exist from v0.23.0 but default disabled at v0.40.4; enable rateLimits.enabled.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a"], "status": "REASONED"}, - "pb-limit-rules": {"text": "Seeded limits are *:auth 2/3s, *:create 20/5s, /api/batch 3/1s and /api/ 300/10s.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a"], "status": "REASONED"}, - "pb-limit-bypass": {"text": "Superusers and rateLimits.excludedIPs bypass limits; test ordinary non-excluded clients.", "components": ["pb"], "sources": ["pb:sf6ffdca4dd39"], "status": "REASONED"}, - "pb-smtp": {"text": "smtp.enabled and smtp.tls default false; disabled SMTP falls back to sendmail. Configure protected transport and test actual delivery.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a", "pb:seb56d3763223"], "status": "REASONED"}, - "appwrite-https": {"text": "API FORCE_HTTPS and function/site ROUTER_FORCE_HTTPS default disabled at 2.2.0; explicitly enable both despite conflicting rolling deprecation/default documentation.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s427bab3733b6", "appwrite-docs:s2bf9a4746d28"], "status": "REASONED"}, - "appwrite-hosts": {"text": "ROUTER_PROTECTION defaults disabled; configure actual domains and enable rejection of unknown hostnames.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s427bab3733b6"], "status": "REASONED"}, - "appwrite-registration": {"text": "Console root-only registration defaults enabled with empty email/IP allowlists; these restrict account creation, not dashboard reachability or login.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s2bb21218c5ee"], "status": "REASONED"}, - "appwrite-console": {"text": "Claim the first operator privately and use a separate network/access boundary for a private dashboard.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s4a965bdeeb98", "appwrite:s2bb21218c5ee"], "status": "REASONED"}, - "appwrite-mfa": {"text": "Enroll and verify operator TOTP, protect recovery codes; at 2.2.0 enabled MFA with a verified factor requires a second session factor.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s221c6aaef8df", "appwrite:s193bfa5c6e22"], "status": "REASONED"}, - "appwrite-dev-env": {"text": "Development env disables root-only registration, abuse and router protection and carries placeholders; do not inherit it for production.", "components": ["appwrite"], "sources": ["appwrite:sadb8c6e7b00a"], "status": "REASONED"}, - "appwrite-recreate": {"text": "Apply env/Compose changes with docker compose up -d from the install directory, then check effective behavior.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s4a965bdeeb98"], "status": "REASONED"}, - "appwrite-permissions": {"text": "Collection and document grants are additive according to legacy documentation; documentSecurity defaults false in Appwrite 2.2.0 and must be enabled for document grants. The pinned parameter description states the need for one permission level.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:sf916851a0b10", "appwrite-docs:s58ad28816c5d"], "status": "REASONED"}, - "appwrite-creation": {"text": "For null or omitted document permissions, the pinned aggregate helper preserves null before the handler assigns read/update/delete user grants to a nonprivileged requester with a user ID, or an empty list otherwise. API-key classification skips role validation but does not itself determine the default list. Use specific users/teams for private data.", "components": ["appwrite", "appwrite-docs", "utopia-php-database"], "sources": ["appwrite:s5b4b3eb2271f", "appwrite:s93647e16b966", "appwrite:sda698c40fc34", "appwrite-docs:sa0bd132f7968", "appwrite:s7ba6f9c71972", "utopia-php-database:s847e0ded0300"], "status": "REASONED"}, - "appwrite-read-path": {"text": "Document GET classifies the requester from authorization roles (keys; owner/developer/admin privileged), reports disabled databases/collections as not found except to those requesters, and fetches the document through the database layer; collection permissions and documentSecurity are passed to the database layer at collection creation.", "components": ["appwrite"], "sources": ["appwrite:s5396e119c026", "appwrite:s82988de645fa", "appwrite:s224cc9d8a387", "appwrite:s1fe0e6185518"], "status": "REASONED"}, - "appwrite-requester": {"text": "With an API key the request role and scopes come from the key; a standard or ephemeral project key for the same project disables the default authorization status, as do admin-mode console sessions outside console project routes, while route scope validation applies to every requester.", "components": ["appwrite"], "sources": ["appwrite:sc55465cf878d", "appwrite:sed1bf9453ecb", "appwrite:s8784d02c82c9", "appwrite:sf60a2b283b80"], "status": "REASONED"}, - "appwrite-keys": {"text": "Properly scoped server API keys bypass resource permissions but still obey operation scopes; keys.write is admin-equivalent and keys never belong in clients.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s193bfa5c6e22", "appwrite-docs:saa37bb5d4d7e"], "status": "REASONED"}, - "appwrite-encryption": {"text": "Replace _APP_OPENSSL_KEY_V1=your-secret-key before production data; preserve the exact key separately, since replacement/loss strands encrypted secrets.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, - "appwrite-backups": {"text": "Back up database, persistent storage and deployment configuration; restrict access and test restoration in a separate installation.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, - "appwrite-abuse": {"text": "_APP_OPTIONS_ABUSE defaults enabled but dev env disables it; server API-key requests are exempt, so test with ordinary clients.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:sadb8c6e7b00a", "appwrite:s193bfa5c6e22", "appwrite-docs:s000d05106450"], "status": "REASONED"}, - "appwrite-smtp": {"text": "SMTP host/port/secure/username/password default empty and empty host disables sending; configure the provider and confirm message receipt/use.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite-docs:s38a1efa0c7d2"], "status": "REASONED"}, - "appwrite-storage": {"text": "Storage defaults local, upload limit 30000000 and server antivirus disabled; enabling scanning requires configured reachable ClamAV.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14"], "status": "REASONED"}, - "appwrite-bucket": {"text": "New bucket permissions are empty and fileSecurity=false; enable per-file security deliberately, since bucket grants also permit access.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3"], "status": "REASONED"}, - "appwrite-bucket-options": {"text": "Bucket encryption and antivirus default true; bucket antivirus does not enable the server scanner.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3"], "status": "REASONED"}, - "appwrite-size": {"text": "Encryption/scanning skip files above 20000000 bytes; cap maximumFileSize at that threshold when either is mandatory and test the boundary.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3", "appwrite:s4ab95ecbf6b5"], "status": "REASONED"}, - "appwrite-functions": {"text": "Function execute roles and execution-key scopes default empty; grant narrowly. Disabled functions still admit authorized Server SDK API keys.", "components": ["appwrite"], "sources": ["appwrite:s655a0abb9ef0"], "status": "REASONED"}, - "appwrite-executor": {"text": "Keep executor/orchestrator private, protect Docker-socket authority and replace _APP_EXECUTOR_SECRET; container execution is not demonstrated hostile-tenant isolation.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s8e5272615f2e", "appwrite-docs:s2bf9a4746d28"], "status": "REASONED"}, - "verify-pb-records": {"text": "Public rules allow guest reads; locked rules deny with 403, unsatisfied list filters can return empty 200 and view rules 404. Retain owner and separate superuser controls.", "components": ["pb-docs", "curl"], "sources": ["pb-docs:saedbdfbdfc48", "curl:s5e1383feef7d", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, - "verify-appwrite-records": {"text": "Broad collection grants defeat document restrictions; removing them must deny unrelated users while document grantees succeed. Compare client/server creation defaults.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s58ad28816c5d", "appwrite-docs:sa0bd132f7968"], "status": "REASONED", "verify": [1]}, - "verify-appwrite-keys": {"text": "A correctly scoped server key reads despite empty permissions, while a key without scope fails; neither substitutes for an ordinary-user control.", "components": ["appwrite"], "sources": ["appwrite:s193bfa5c6e22"], "status": "REASONED", "verify": [1]}, - "verify-registration": {"text": "On disposable Appwrite fixtures compare open signup 201 with root-only account-limit failure and independent email/IP restrictions; retain login/invitation controls.", "components": ["appwrite"], "sources": ["appwrite:s2bb21218c5ee"], "status": "REASONED", "verify": [2]}, - "verify-pb-bootstrap": {"text": "Guest and ordinary-user _superusers record creation must fail while a valid installer token creates the private operator; static dashboard assets prove no admin access.", "components": ["pb"], "sources": ["pb:saf684a26056b", "pb:s69aa223f52e4"], "status": "REASONED"}, - "verify-appwrite-mfa": {"text": "Console account GET succeeds without MFA, fails with user_more_factors_required for a fresh password-only MFA session, and succeeds after TOTP completion.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s221c6aaef8df", "appwrite:s193bfa5c6e22"], "status": "REASONED"}, - "verify-pb-writes": {"text": "Compare intended/unauthorized POST/PATCH/DELETE and privileged auth-record management; locked manageRule grants no management and empty-string configuration must fail.", "components": ["pb"], "sources": ["pb:s69aa223f52e4", "pb:sce114368c2ad"], "status": "REASONED"}, - "verify-pb-files": {"text": "Before protection unsigned file GET works; Protected plus private viewRule rejects guests/unrelated-user file tokens with 404 while permitted-user bytes still arrive.", "components": ["pb", "curl"], "sources": ["pb:sa5e571f8d473", "curl:s82a34ee18632"], "status": "REASONED"}, - "verify-pb-realtime": {"text": "Mutate a fixture while comparing collection and record subscriptions; deny unauthorized event delivery, retaining authorized delivery. PB_CONNECT/acknowledgment alone is insufficient.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sb850a71fad4d", "pb:sfd2f779e8e11"], "status": "REASONED"}, - "verify-pb-mfa": {"text": "Compare password-only completion with password-plus-OTP MFA and verified/unverified authRule logins; clearing authStore must not revoke a retained valid token.", "components": ["pb-docs"], "sources": ["pb-docs:s773e441dd51c"], "status": "REASONED"}, - "verify-pb-ips": {"text": "Compare allowed/excluded superuser sources and forged forwarded headers; excluded sources must get 403 and direct backend access must fail with a working proxy control.", "components": ["pb"], "sources": ["pb:s5b66ca69bf6d", "pb:s03dd7dd84c2a"], "status": "REASONED"}, - "verify-transport": {"text": "Compare HTTP/HTTPS and certificates, Appwrite API/function/site domains and configured/unknown Host; compare browser origins separately from record authorization.", "components": ["pb", "appwrite"], "sources": ["pb:s4b155ef30262", "appwrite:s427bab3733b6"], "status": "REASONED"}, - "verify-abuse": {"text": "Cross each configured threshold with ordinary non-excluded clients and compare disabled/enabled controls; PocketBase limiting yields 429 with later successful recovery.", "components": ["pb", "appwrite-docs"], "sources": ["pb:sf6ffdca4dd39", "appwrite-docs:s000d05106450"], "status": "REASONED"}, - "verify-email": {"text": "Request OTP, verification and recovery messages and confirm receipt and use; request success alone is not delivery evidence.", "components": ["pb", "appwrite-docs"], "sources": ["pb:seb56d3763223", "appwrite-docs:s38a1efa0c7d2"], "status": "REASONED"}, - "verify-pb-recovery": {"text": "Compare plaintext/encrypted persisted settings, restart with retained key and restore local/S3 data; only permitted superuser file tokens may download backups.", "components": ["pb", "pb-docs"], "sources": ["pb:s03dd7dd84c2a", "pb:s31a5adccd695", "pb-docs:s9029ab974cf1"], "status": "REASONED"}, - "verify-appwrite-recovery": {"text": "Restore encrypted fixtures with the original key; a separate wrong-key fixture must not recover encrypted values.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, - "verify-uploads": {"text": "Compare permitted/unrelated uploads and size-boundary fixtures; mandatory processing requires rejecting over-limit files and confirming accepted-file encryption/scanning.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3", "appwrite:s4ab95ecbf6b5"], "status": "REASONED"}, - "verify-functions": {"text": "Compare intended execute roles, unrelated users and privileged server keys, including disabled functions; confirm executor/orchestrator privacy from a second host.", "components": ["appwrite"], "sources": ["appwrite:s655a0abb9ef0", "appwrite:s8e5272615f2e"], "status": "REASONED"}, - "verify-bundle": {"text": "Search filenames using protected exact-secret patterns with a disposable marker control; client bundles must contain neither superuser tokens nor Appwrite API keys, and encoded/split forms can evade scanning.", "components": ["pb-docs", "appwrite-docs"], "sources": ["pb-docs:s773e441dd51c", "appwrite-docs:saa37bb5d4d7e"], "status": "REASONED", "verify": [3]}, - "local-guards": {"text": "Recorded guard tests rejected placeholders and omitted/shortened arguments; this demonstrates shell behavior only, not service access or deployed-bundle scanning.", "components": ["pb-docs", "appwrite-docs"], "sources": ["pb-docs:saedbdfbdfc48", "appwrite-docs:sa0bd132f7968", "appwrite-docs:saa37bb5d4d7e"], "status": "DEMONSTRATED", "evidence": "Local guard tests refused embedded `REPLACE_WITH_` placeholders, `example.com`, angle brackets, empty arguments, and omitted or shortened `set --` lines."}, - "local-syntax": {"text": "All three bash fences have recorded local lint/syntax success, not live service evidence; ShellCheck version is recorded only in the body.", "components": ["appwrite", "pb-docs", "appwrite-docs"], "sources": ["pb-docs:saedbdfbdfc48", "appwrite:s2bb21218c5ee", "appwrite-docs:saa37bb5d4d7e"], "status": "DEMONSTRATED", "evidence": "All three bash blocks passed ShellCheck 0.11.0 and `bash -n` during authoring."}, - "source-limits": {"text": "The PocketBase CLI wrapper, --dev default, and Appwrite requester classification and document-read call sites are now cited; how utopia-php/database matches roles against collection/document permissions (including documentSecurity) and what the disabled default authorization status bypasses remain untraced in that unfetched dependency.", "components": ["appwrite-docs", "pb-docs", "appwrite"], "sources": ["appwrite-docs:s58ad28816c5d", "pb-docs:s9029ab974cf1", "appwrite:s7ba6f9c71972"], "status": "REASONED"} - } -} ---- # Self-hosted backends: PocketBase and Appwrite - -**Version basis** +Written against PocketBase v0.40.4, Appwrite 2.2.0, utopia-php/database 7.3.9, and curl curl-8_14_1; 2 further sources carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 75 claims: 2 demonstrated by a check that ran and 73 reasoned from cited sources. -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +Like Firebase and Supabase ([firebase-supabase.md](firebase-supabase.md)), these backends hand a +public API endpoint to your client code; the collection or resource rules you write gate access +to your data. Files and privileged server credentials need separate attention. Both also ship +an admin console, so complete bootstrap privately before exposing the instance. Appwrite's first +console registrant becomes its initial operator; PocketBase's installer requires a privileged +token from the startup log, not merely an ordinary user's registration. -Components (recorded basis): +The source checks below target **PocketBase v0.40.4** and **Appwrite 2.2.0**, with documentation +checked in September 2026. Defaults refer to those releases unless stated otherwise. Live behavior +has not been demonstrated in this authoring environment. Some pin-specific source checks remain +incomplete; the source-work table below records those. Live comparisons are REASONED from the +cited vendor documentation and the pinned sources that were available. -- pb: PocketBase (v0.40.4) -- appwrite: Appwrite (2.2.0) -- pb-docs: PocketBase documentation (rolling) (unknown) -- appwrite-docs: Appwrite documentation (rolling) (unknown) -- curl: curl (curl-8_14_1) -- utopia-php-database: utopia-php/database (7.3.9) +## PocketBase -Claims (full source mapping in the front matter): - -- pb-bootstrap (REASONED; pb, pb-docs): Privately bootstrap through the logged installer URL; superuser create EMAIL PASS exposes passwords in argv. Ordinary registration does not grant superuser rights. -- pb-installer (REASONED; pb): Installer system-superuser token lasts about 30 minutes; protect its URL and startup logs as credentials. -- pb-ips (REASONED; pb, pb-docs): Superuser IP allowlists are available from v0.38.0; superuserIPs defaults empty and restricts authenticated requests, not /_/ static assets. -- pb-tls (REASONED; pb): serve with a domain provides native HTTPS/ACME; alternatively terminate TLS at a reverse proxy. -- pb-bind (REASONED; pb): Without a domain serve defaults to 127.0.0.1:8090; with a domain it uses 0.0.0.0:80/443. Behind a local proxy omit the domain and set --http loopback. -- pb-origins (REASONED; pb): --origins defaults * and accepts comma-separated allowed origins; CORS does not authorize records. -- pb-proxy (REASONED; pb): Trust only headers the proxy overwrites, review useLeftmostIP ordering and block direct backend access to protect IP/rate controls. -- pb-dev (REASONED; pb): --dev adds diagnostics including SQL on stderr, not an auth bypass; its configured default comes from New(): true if os.Args[0] starts with a nonempty os.TempDir() or cacheDir() prefix, false otherwise. -- pb-cli (REASONED; pb): In the pinned CLI wrapper, Start() registers the serve and superuser commands on the root command and executes it. -- pb-rules (REASONED; pb, pb-docs): Each list/view/create/update/delete rule defaults null (superuser-only); empty opens to guests and nonempty filters. Superusers bypass rules. -- pb-manage (REASONED; pb): Top-level manageRule defaults null and permits privileged auth-record changes alongside create/update rules; its validator rejects empty-string rules. -- pb-files (REASONED; pb, pb-docs): File fields default unprotected despite locked record rules; Protected plus an authorized viewRule gates downloads, while a public viewRule still permits public access. -- pb-file-token (REASONED; pb, pb-docs): Short-lived file tokens provide identity context, not file authorization; viewRule decides. Protect token-bearing file URLs. -- pb-realtime (REASONED; pb, pb-docs): Collection subscriptions use listRule and individual-record subscriptions viewRule; SSE/subscription success does not prove event authorization. -- pb-password (REASONED; pb): Auth initialization enables password authentication with email as the identity field. -- pb-oauth (REASONED; pb, pb-docs): OAuth2 defaults disabled and is unsupported for _superusers. -- pb-mfa-default (REASONED; pb): MFA defaults disabled with duration 600 seconds. -- pb-otp-default (REASONED; pb): OTP defaults disabled with duration 180 seconds and length 8. -- pb-alerts (REASONED; pb): Authentication alerts default enabled; retain alerts and provide working email. -- pb-auth-token (REASONED; pb): Auth token duration defaults to 432000 seconds. -- pb-file-duration (REASONED; pb): File token duration defaults to 180 seconds. -- pb-auth-rule (REASONED; pb, pb-docs): authRule defaults empty; verified-only collections can require verified = true after email verification works. -- pb-mfa (REASONED; pb, pb-docs): For superusers retain password auth and enable both OTP and MFA; empty mfa.rule applies to everyone. OTP alone is not a second factor. -- pb-logout (REASONED; pb, pb-docs): Stateless authentication means clearing pb.authStore removes only the local copy, not a stolen token. -- pb-encryption (REASONED; pb, pb-docs): Settings default to plaintext JSON; select a 32-character environment secret with --encryptionEnv. This encrypts settings, not the whole database or files. -- pb-backups (REASONED; pb, pb-docs): Backups default local with empty cron (automatic backups off); archives contain main and logs databases plus remaining pb_data, excluding the backups directory and tracked exclusions; uploaded collection files on S3 require manual backup. Schedule/retain deliberately and protect archives. -- pb-backup-auth (REASONED; pb): Backup downloads require a superuser file token and obey superuser IP restrictions; ordinary auth/file tokens do not suffice. -- pb-limiter (REASONED; pb, pb-docs): Native limits exist from v0.23.0 but default disabled at v0.40.4; enable rateLimits.enabled. -- pb-limit-rules (REASONED; pb): Seeded limits are *:auth 2/3s, *:create 20/5s, /api/batch 3/1s and /api/ 300/10s. -- pb-limit-bypass (REASONED; pb): Superusers and rateLimits.excludedIPs bypass limits; test ordinary non-excluded clients. -- pb-smtp (REASONED; pb): smtp.enabled and smtp.tls default false; disabled SMTP falls back to sendmail. Configure protected transport and test actual delivery. -- appwrite-https (REASONED; appwrite, appwrite-docs): API FORCE_HTTPS and function/site ROUTER_FORCE_HTTPS default disabled at 2.2.0; explicitly enable both despite conflicting rolling deprecation/default documentation. -- appwrite-hosts (REASONED; appwrite): ROUTER_PROTECTION defaults disabled; configure actual domains and enable rejection of unknown hostnames. -- appwrite-registration (REASONED; appwrite): Console root-only registration defaults enabled with empty email/IP allowlists; these restrict account creation, not dashboard reachability or login. -- appwrite-console (REASONED; appwrite, appwrite-docs): Claim the first operator privately and use a separate network/access boundary for a private dashboard. -- appwrite-mfa (REASONED; appwrite, appwrite-docs): Enroll and verify operator TOTP, protect recovery codes; at 2.2.0 enabled MFA with a verified factor requires a second session factor. -- appwrite-dev-env (REASONED; appwrite): Development env disables root-only registration, abuse and router protection and carries placeholders; do not inherit it for production. -- appwrite-recreate (REASONED; appwrite-docs): Apply env/Compose changes with docker compose up -d from the install directory, then check effective behavior. -- appwrite-permissions (REASONED; appwrite, appwrite-docs): Collection and document grants are additive according to legacy documentation; documentSecurity defaults false in Appwrite 2.2.0 and must be enabled for document grants. The pinned parameter description states the need for one permission level. -- appwrite-creation (REASONED; appwrite, appwrite-docs, utopia-php-database): For null or omitted document permissions, the pinned aggregate helper preserves null before the handler assigns read/update/delete user grants to a nonprivileged requester with a user ID, or an empty list otherwise. API-key classification skips role validation but does not itself determine the default list. Use specific users/teams for private data. -- appwrite-read-path (REASONED; appwrite): Document GET classifies the requester from authorization roles (keys; owner/developer/admin privileged), reports disabled databases/collections as not found except to those requesters, and fetches the document through the database layer; collection permissions and documentSecurity are passed to the database layer at collection creation. -- appwrite-requester (REASONED; appwrite): With an API key the request role and scopes come from the key; a standard or ephemeral project key for the same project disables the default authorization status, as do admin-mode console sessions outside console project routes, while route scope validation applies to every requester. -- appwrite-keys (REASONED; appwrite, appwrite-docs): Properly scoped server API keys bypass resource permissions but still obey operation scopes; keys.write is admin-equivalent and keys never belong in clients. -- appwrite-encryption (REASONED; appwrite, appwrite-docs): Replace _APP_OPENSSL_KEY_V1=your-secret-key before production data; preserve the exact key separately, since replacement/loss strands encrypted secrets. -- appwrite-backups (REASONED; appwrite-docs): Back up database, persistent storage and deployment configuration; restrict access and test restoration in a separate installation. -- appwrite-abuse (REASONED; appwrite, appwrite-docs): _APP_OPTIONS_ABUSE defaults enabled but dev env disables it; server API-key requests are exempt, so test with ordinary clients. -- appwrite-smtp (REASONED; appwrite, appwrite-docs): SMTP host/port/secure/username/password default empty and empty host disables sending; configure the provider and confirm message receipt/use. -- appwrite-storage (REASONED; appwrite): Storage defaults local, upload limit 30000000 and server antivirus disabled; enabling scanning requires configured reachable ClamAV. -- appwrite-bucket (REASONED; appwrite): New bucket permissions are empty and fileSecurity=false; enable per-file security deliberately, since bucket grants also permit access. -- appwrite-bucket-options (REASONED; appwrite): Bucket encryption and antivirus default true; bucket antivirus does not enable the server scanner. -- appwrite-size (REASONED; appwrite): Encryption/scanning skip files above 20000000 bytes; cap maximumFileSize at that threshold when either is mandatory and test the boundary. -- appwrite-functions (REASONED; appwrite): Function execute roles and execution-key scopes default empty; grant narrowly. Disabled functions still admit authorized Server SDK API keys. -- appwrite-executor (REASONED; appwrite, appwrite-docs): Keep executor/orchestrator private, protect Docker-socket authority and replace _APP_EXECUTOR_SECRET; container execution is not demonstrated hostile-tenant isolation. -- verify-pb-records (REASONED; pb-docs, curl): Public rules allow guest reads; locked rules deny with 403, unsatisfied list filters can return empty 200 and view rules 404. Retain owner and separate superuser controls. -- verify-appwrite-records (REASONED; appwrite-docs): Broad collection grants defeat document restrictions; removing them must deny unrelated users while document grantees succeed. Compare client/server creation defaults. -- verify-appwrite-keys (REASONED; appwrite): A correctly scoped server key reads despite empty permissions, while a key without scope fails; neither substitutes for an ordinary-user control. -- verify-registration (REASONED; appwrite): On disposable Appwrite fixtures compare open signup 201 with root-only account-limit failure and independent email/IP restrictions; retain login/invitation controls. -- verify-pb-bootstrap (REASONED; pb): Guest and ordinary-user _superusers record creation must fail while a valid installer token creates the private operator; static dashboard assets prove no admin access. -- verify-appwrite-mfa (REASONED; appwrite, appwrite-docs): Console account GET succeeds without MFA, fails with user_more_factors_required for a fresh password-only MFA session, and succeeds after TOTP completion. -- verify-pb-writes (REASONED; pb): Compare intended/unauthorized POST/PATCH/DELETE and privileged auth-record management; locked manageRule grants no management and empty-string configuration must fail. -- verify-pb-files (REASONED; pb, curl): Before protection unsigned file GET works; Protected plus private viewRule rejects guests/unrelated-user file tokens with 404 while permitted-user bytes still arrive. -- verify-pb-realtime (REASONED; pb, pb-docs): Mutate a fixture while comparing collection and record subscriptions; deny unauthorized event delivery, retaining authorized delivery. PB_CONNECT/acknowledgment alone is insufficient. -- verify-pb-mfa (REASONED; pb-docs): Compare password-only completion with password-plus-OTP MFA and verified/unverified authRule logins; clearing authStore must not revoke a retained valid token. -- verify-pb-ips (REASONED; pb): Compare allowed/excluded superuser sources and forged forwarded headers; excluded sources must get 403 and direct backend access must fail with a working proxy control. -- verify-transport (REASONED; pb, appwrite): Compare HTTP/HTTPS and certificates, Appwrite API/function/site domains and configured/unknown Host; compare browser origins separately from record authorization. -- verify-abuse (REASONED; pb, appwrite-docs): Cross each configured threshold with ordinary non-excluded clients and compare disabled/enabled controls; PocketBase limiting yields 429 with later successful recovery. -- verify-email (REASONED; pb, appwrite-docs): Request OTP, verification and recovery messages and confirm receipt and use; request success alone is not delivery evidence. -- verify-pb-recovery (REASONED; pb, pb-docs): Compare plaintext/encrypted persisted settings, restart with retained key and restore local/S3 data; only permitted superuser file tokens may download backups. -- verify-appwrite-recovery (REASONED; appwrite-docs): Restore encrypted fixtures with the original key; a separate wrong-key fixture must not recover encrypted values. -- verify-uploads (REASONED; appwrite): Compare permitted/unrelated uploads and size-boundary fixtures; mandatory processing requires rejecting over-limit files and confirming accepted-file encryption/scanning. -- verify-functions (REASONED; appwrite): Compare intended execute roles, unrelated users and privileged server keys, including disabled functions; confirm executor/orchestrator privacy from a second host. -- verify-bundle (REASONED; pb-docs, appwrite-docs): Search filenames using protected exact-secret patterns with a disposable marker control; client bundles must contain neither superuser tokens nor Appwrite API keys, and encoded/split forms can evade scanning. -- local-guards (DEMONSTRATED; pb-docs, appwrite-docs): Recorded guard tests rejected placeholders and omitted/shortened arguments; this demonstrates shell behavior only, not service access or deployed-bundle scanning. -- local-syntax (DEMONSTRATED; appwrite, pb-docs, appwrite-docs): All three bash fences have recorded local lint/syntax success, not live service evidence; ShellCheck version is recorded only in the body. -- source-limits (REASONED; appwrite-docs, pb-docs, appwrite): The PocketBase CLI wrapper, --dev default, and Appwrite requester classification and document-read call sites are now cited; how utopia-php/database matches roles against collection/document permissions (including documentSecurity) and what the disabled default authorization status bypasses remain untraced in that unfetched dependency. - - -Like Firebase and Supabase ([firebase-supabase.md](firebase-supabase.md)), these backends hand a -public API endpoint to your client code; the collection or resource rules you write gate access -to your data. Files and privileged server credentials need separate attention. Both also ship -an admin console, so complete bootstrap privately before exposing the instance. Appwrite's first -console registrant becomes its initial operator; PocketBase's installer requires a privileged -token from the startup log, not merely an ordinary user's registration. - -The source checks below target **PocketBase v0.40.4** and **Appwrite 2.2.0**, with documentation -checked in September 2026. Defaults refer to those releases unless stated otherwise. Live behavior -has not been demonstrated in this authoring environment. Some pin-specific source checks remain -incomplete; the source-work table below records those. Live comparisons are REASONED from the -cited vendor documentation and the pinned sources that were available. - -## PocketBase - -### 1. Bootstrap privately and protect the operator +### 1. Bootstrap privately and protect the operator Create the superuser before opening access. The documented console syntax is `./pocketbase superuser create EMAIL PASS`; the alternative is the web-based installer linked from @@ -966,3 +691,304 @@ service result or deployed-bundle scan is claimed. - [curl curl-8_14_1 write-out fields and version requirements (checked October 2026)](https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79). - [curl curl-8_14_1 headers from stdin (checked October 2026)](https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md). - [curl curl-8_14_1 configuration from stdin (checked October 2026)](https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/config.md). + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/pocketbase.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- pb: PocketBase (v0.40.4) +- appwrite: Appwrite (2.2.0) +- pb-docs: PocketBase documentation (rolling) (unknown) +- appwrite-docs: Appwrite documentation (rolling) (unknown) +- utopia-php-database: utopia-php/database (7.3.9) +- curl: curl (curl-8_14_1) + +Claims (full source mapping in the JSON block below): + +- pb-bootstrap (REASONED; pb, pb-docs): Privately bootstrap through the logged installer URL; superuser create EMAIL PASS exposes passwords in argv. Ordinary registration does not grant superuser rights. +- pb-installer (REASONED; pb): Installer system-superuser token lasts about 30 minutes; protect its URL and startup logs as credentials. +- pb-ips (REASONED; pb, pb-docs): Superuser IP allowlists are available from v0.38.0; superuserIPs defaults empty and restricts authenticated requests, not /_/ static assets. +- pb-tls (REASONED; pb): serve with a domain provides native HTTPS/ACME; alternatively terminate TLS at a reverse proxy. +- pb-bind (REASONED; pb): Without a domain serve defaults to 127.0.0.1:8090; with a domain it uses 0.0.0.0:80/443. Behind a local proxy omit the domain and set --http loopback. +- pb-origins (REASONED; pb): --origins defaults * and accepts comma-separated allowed origins; CORS does not authorize records. +- pb-proxy (REASONED; pb): Trust only headers the proxy overwrites, review useLeftmostIP ordering and block direct backend access to protect IP/rate controls. +- pb-dev (REASONED; pb): --dev adds diagnostics including SQL on stderr, not an auth bypass; its configured default comes from New(): true if os.Args[0] starts with a nonempty os.TempDir() or cacheDir() prefix, false otherwise. +- pb-cli (REASONED; pb): In the pinned CLI wrapper, Start() registers the serve and superuser commands on the root command and executes it. +- pb-rules (REASONED; pb, pb-docs): Each list/view/create/update/delete rule defaults null (superuser-only); empty opens to guests and nonempty filters. Superusers bypass rules. +- pb-manage (REASONED; pb): Top-level manageRule defaults null and permits privileged auth-record changes alongside create/update rules; its validator rejects empty-string rules. +- pb-files (REASONED; pb, pb-docs): File fields default unprotected despite locked record rules; Protected plus an authorized viewRule gates downloads, while a public viewRule still permits public access. +- pb-file-token (REASONED; pb, pb-docs): Short-lived file tokens provide identity context, not file authorization; viewRule decides. Protect token-bearing file URLs. +- pb-realtime (REASONED; pb, pb-docs): Collection subscriptions use listRule and individual-record subscriptions viewRule; SSE/subscription success does not prove event authorization. +- pb-password (REASONED; pb): Auth initialization enables password authentication with email as the identity field. +- pb-oauth (REASONED; pb, pb-docs): OAuth2 defaults disabled and is unsupported for _superusers. +- pb-mfa-default (REASONED; pb): MFA defaults disabled with duration 600 seconds. +- pb-otp-default (REASONED; pb): OTP defaults disabled with duration 180 seconds and length 8. +- pb-alerts (REASONED; pb): Authentication alerts default enabled; retain alerts and provide working email. +- pb-auth-token (REASONED; pb): Auth token duration defaults to 432000 seconds. +- pb-file-duration (REASONED; pb): File token duration defaults to 180 seconds. +- pb-auth-rule (REASONED; pb, pb-docs): authRule defaults empty; verified-only collections can require verified = true after email verification works. +- pb-mfa (REASONED; pb, pb-docs): For superusers retain password auth and enable both OTP and MFA; empty mfa.rule applies to everyone. OTP alone is not a second factor. +- pb-logout (REASONED; pb, pb-docs): Stateless authentication means clearing pb.authStore removes only the local copy, not a stolen token. +- pb-encryption (REASONED; pb, pb-docs): Settings default to plaintext JSON; select a 32-character environment secret with --encryptionEnv. This encrypts settings, not the whole database or files. +- pb-backups (REASONED; pb, pb-docs): Backups default local with empty cron (automatic backups off); archives contain main and logs databases plus remaining pb_data, excluding the backups directory and tracked exclusions; uploaded collection files on S3 require manual backup. Schedule/retain deliberately and protect archives. +- pb-backup-auth (REASONED; pb): Backup downloads require a superuser file token and obey superuser IP restrictions; ordinary auth/file tokens do not suffice. +- pb-limiter (REASONED; pb, pb-docs): Native limits exist from v0.23.0 but default disabled at v0.40.4; enable rateLimits.enabled. +- pb-limit-rules (REASONED; pb): Seeded limits are *:auth 2/3s, *:create 20/5s, /api/batch 3/1s and /api/ 300/10s. +- pb-limit-bypass (REASONED; pb): Superusers and rateLimits.excludedIPs bypass limits; test ordinary non-excluded clients. +- pb-smtp (REASONED; pb): smtp.enabled and smtp.tls default false; disabled SMTP falls back to sendmail. Configure protected transport and test actual delivery. +- appwrite-https (REASONED; appwrite, appwrite-docs): API FORCE_HTTPS and function/site ROUTER_FORCE_HTTPS default disabled at 2.2.0; explicitly enable both despite conflicting rolling deprecation/default documentation. +- appwrite-hosts (REASONED; appwrite): ROUTER_PROTECTION defaults disabled; configure actual domains and enable rejection of unknown hostnames. +- appwrite-registration (REASONED; appwrite): Console root-only registration defaults enabled with empty email/IP allowlists; these restrict account creation, not dashboard reachability or login. +- appwrite-console (REASONED; appwrite, appwrite-docs): Claim the first operator privately and use a separate network/access boundary for a private dashboard. +- appwrite-mfa (REASONED; appwrite, appwrite-docs): Enroll and verify operator TOTP, protect recovery codes; at 2.2.0 enabled MFA with a verified factor requires a second session factor. +- appwrite-dev-env (REASONED; appwrite): Development env disables root-only registration, abuse and router protection and carries placeholders; do not inherit it for production. +- appwrite-recreate (REASONED; appwrite-docs): Apply env/Compose changes with docker compose up -d from the install directory, then check effective behavior. +- appwrite-permissions (REASONED; appwrite, appwrite-docs): Collection and document grants are additive according to legacy documentation; documentSecurity defaults false in Appwrite 2.2.0 and must be enabled for document grants. The pinned parameter description states the need for one permission level. +- appwrite-creation (REASONED; appwrite, appwrite-docs, utopia-php-database): For null or omitted document permissions, the pinned aggregate helper preserves null before the handler assigns read/update/delete user grants to a nonprivileged requester with a user ID, or an empty list otherwise. API-key classification skips role validation but does not itself determine the default list. Use specific users/teams for private data. +- appwrite-read-path (REASONED; appwrite): Document GET classifies the requester from authorization roles (keys; owner/developer/admin privileged), reports disabled databases/collections as not found except to those requesters, and fetches the document through the database layer; collection permissions and documentSecurity are passed to the database layer at collection creation. +- appwrite-requester (REASONED; appwrite): With an API key the request role and scopes come from the key; a standard or ephemeral project key for the same project disables the default authorization status, as do admin-mode console sessions outside console project routes, while route scope validation applies to every requester. +- appwrite-keys (REASONED; appwrite, appwrite-docs): Properly scoped server API keys bypass resource permissions but still obey operation scopes; keys.write is admin-equivalent and keys never belong in clients. +- appwrite-encryption (REASONED; appwrite, appwrite-docs): Replace _APP_OPENSSL_KEY_V1=your-secret-key before production data; preserve the exact key separately, since replacement/loss strands encrypted secrets. +- appwrite-backups (REASONED; appwrite-docs): Back up database, persistent storage and deployment configuration; restrict access and test restoration in a separate installation. +- appwrite-abuse (REASONED; appwrite, appwrite-docs): _APP_OPTIONS_ABUSE defaults enabled but dev env disables it; server API-key requests are exempt, so test with ordinary clients. +- appwrite-smtp (REASONED; appwrite, appwrite-docs): SMTP host/port/secure/username/password default empty and empty host disables sending; configure the provider and confirm message receipt/use. +- appwrite-storage (REASONED; appwrite): Storage defaults local, upload limit 30000000 and server antivirus disabled; enabling scanning requires configured reachable ClamAV. +- appwrite-bucket (REASONED; appwrite): New bucket permissions are empty and fileSecurity=false; enable per-file security deliberately, since bucket grants also permit access. +- appwrite-bucket-options (REASONED; appwrite): Bucket encryption and antivirus default true; bucket antivirus does not enable the server scanner. +- appwrite-size (REASONED; appwrite): Encryption/scanning skip files above 20000000 bytes; cap maximumFileSize at that threshold when either is mandatory and test the boundary. +- appwrite-functions (REASONED; appwrite): Function execute roles and execution-key scopes default empty; grant narrowly. Disabled functions still admit authorized Server SDK API keys. +- appwrite-executor (REASONED; appwrite, appwrite-docs): Keep executor/orchestrator private, protect Docker-socket authority and replace _APP_EXECUTOR_SECRET; container execution is not demonstrated hostile-tenant isolation. +- verify-pb-records (REASONED; pb-docs, curl): Public rules allow guest reads; locked rules deny with 403, unsatisfied list filters can return empty 200 and view rules 404. Retain owner and separate superuser controls. +- verify-appwrite-records (REASONED; appwrite-docs): Broad collection grants defeat document restrictions; removing them must deny unrelated users while document grantees succeed. Compare client/server creation defaults. +- verify-appwrite-keys (REASONED; appwrite): A correctly scoped server key reads despite empty permissions, while a key without scope fails; neither substitutes for an ordinary-user control. +- verify-registration (REASONED; appwrite): On disposable Appwrite fixtures compare open signup 201 with root-only account-limit failure and independent email/IP restrictions; retain login/invitation controls. +- verify-pb-bootstrap (REASONED; pb): Guest and ordinary-user _superusers record creation must fail while a valid installer token creates the private operator; static dashboard assets prove no admin access. +- verify-appwrite-mfa (REASONED; appwrite, appwrite-docs): Console account GET succeeds without MFA, fails with user_more_factors_required for a fresh password-only MFA session, and succeeds after TOTP completion. +- verify-pb-writes (REASONED; pb): Compare intended/unauthorized POST/PATCH/DELETE and privileged auth-record management; locked manageRule grants no management and empty-string configuration must fail. +- verify-pb-files (REASONED; pb, curl): Before protection unsigned file GET works; Protected plus private viewRule rejects guests/unrelated-user file tokens with 404 while permitted-user bytes still arrive. +- verify-pb-realtime (REASONED; pb, pb-docs): Mutate a fixture while comparing collection and record subscriptions; deny unauthorized event delivery, retaining authorized delivery. PB_CONNECT/acknowledgment alone is insufficient. +- verify-pb-mfa (REASONED; pb-docs): Compare password-only completion with password-plus-OTP MFA and verified/unverified authRule logins; clearing authStore must not revoke a retained valid token. +- verify-pb-ips (REASONED; pb): Compare allowed/excluded superuser sources and forged forwarded headers; excluded sources must get 403 and direct backend access must fail with a working proxy control. +- verify-transport (REASONED; pb, appwrite): Compare HTTP/HTTPS and certificates, Appwrite API/function/site domains and configured/unknown Host; compare browser origins separately from record authorization. +- verify-abuse (REASONED; pb, appwrite-docs): Cross each configured threshold with ordinary non-excluded clients and compare disabled/enabled controls; PocketBase limiting yields 429 with later successful recovery. +- verify-email (REASONED; pb, appwrite-docs): Request OTP, verification and recovery messages and confirm receipt and use; request success alone is not delivery evidence. +- verify-pb-recovery (REASONED; pb, pb-docs): Compare plaintext/encrypted persisted settings, restart with retained key and restore local/S3 data; only permitted superuser file tokens may download backups. +- verify-appwrite-recovery (REASONED; appwrite-docs): Restore encrypted fixtures with the original key; a separate wrong-key fixture must not recover encrypted values. +- verify-uploads (REASONED; appwrite): Compare permitted/unrelated uploads and size-boundary fixtures; mandatory processing requires rejecting over-limit files and confirming accepted-file encryption/scanning. +- verify-functions (REASONED; appwrite): Compare intended execute roles, unrelated users and privileged server keys, including disabled functions; confirm executor/orchestrator privacy from a second host. +- verify-bundle (REASONED; pb-docs, appwrite-docs): Search filenames using protected exact-secret patterns with a disposable marker control; client bundles must contain neither superuser tokens nor Appwrite API keys, and encoded/split forms can evade scanning. +- local-guards (DEMONSTRATED; pb-docs, appwrite-docs): Recorded guard tests rejected placeholders and omitted/shortened arguments; this demonstrates shell behavior only, not service access or deployed-bundle scanning. +- local-syntax (DEMONSTRATED; appwrite, pb-docs, appwrite-docs): All three bash fences have recorded local lint/syntax success, not live service evidence; ShellCheck version is recorded only in the body. +- source-limits (REASONED; appwrite-docs, pb-docs, appwrite): The PocketBase CLI wrapper, --dev default, and Appwrite requester classification and document-read call sites are now cited; how utopia-php/database matches roles against collection/document permissions (including documentSecurity) and what the disabled default authorization status bypasses remain untraced in that unfetched dependency. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "56cf568348e9a9e7890afd9133b526447b70bf593b07da5b93bedb63ce116e85", + "components": { + "pb": { + "name": "PocketBase", + "basis": "v0.40.4", + "sources": { + "saf684a26056b": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/installer.go", + "s4b155ef30262": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/cmd/serve.go", + "sb2129128b86c": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/serve.go", + "s5b66ca69bf6d": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/middlewares.go", + "s04ae861b5709": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/record_query.go", + "s69aa223f52e4": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/record_crud.go", + "sce114368c2ad": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/collection_model_auth_options.go", + "s03dd7dd84c2a": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/settings_model.go", + "seb56d3763223": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/base.go", + "sa5e571f8d473": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/file.go", + "sfd2f779e8e11": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/realtime.go", + "s31a5adccd695": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/backup.go", + "se74188862a55": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/backup_create.go#L36-L60", + "sf6ffdca4dd39": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/middlewares_rate_limit.go", + "s1b04c2cd16ec": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L234-L239", + "sb63f49346abe": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L73-L79", + "sd700e81880f2": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L129-L139", + "s54430c83a38a": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L165-L173", + "s2c8f78f2d5ac": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/tools/osutils/run.go#L10-L19", + "s5b0dd2fd7a6a": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/base.go#L1277-L1286", + "sf7c2fb5e78e7": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/base.go#L1489-L1499" + } + }, + "appwrite": { + "name": "Appwrite", + "basis": "2.2.0", + "sources": { + "s9f5bd3b70f14": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/config/variables.php", + "sadb8c6e7b00a": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/.env", + "s2bb21218c5ee": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/api/account.php", + "s193bfa5c6e22": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/shared/api.php", + "s427bab3733b6": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/general.php", + "se9480e5807b3": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Create.php", + "sf916851a0b10": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php#L82-L84", + "s5b4b3eb2271f": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L129-L134", + "s93647e16b966": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L211-L212", + "sda698c40fc34": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L237-L287", + "s655a0abb9ef0": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php", + "s4ab95ecbf6b5": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/init/constants.php", + "s8e5272615f2e": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/docker-compose.yml", + "s7ba6f9c71972": "https://github.com/appwrite/appwrite/blob/2.2.0/composer.lock#L3461-L3471", + "s5396e119c026": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php#L80-L95", + "s82988de645fa": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php#L103-L120", + "s224cc9d8a387": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Utopia/Database/Documents/User.php#L75-L109", + "sc55465cf878d": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L144-L161", + "sed1bf9453ecb": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L312-L339", + "s8784d02c82c9": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L357-L361", + "sf60a2b283b80": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L503-L507", + "s1fe0e6185518": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php#L221-L227" + } + }, + "pb-docs": { + "name": "PocketBase documentation (rolling)", + "basis": "unknown", + "sources": { + "s9029ab974cf1": "https://pocketbase.io/docs/going-to-production/", + "saedbdfbdfc48": "https://pocketbase.io/docs/api-rules-and-filters/", + "s773e441dd51c": "https://pocketbase.io/docs/authentication/", + "sf711b6345643": "https://pocketbase.io/docs/files-handling/", + "sb850a71fad4d": "https://pocketbase.io/docs/api-realtime/" + } + }, + "appwrite-docs": { + "name": "Appwrite documentation (rolling)", + "basis": "unknown", + "sources": { + "s4a965bdeeb98": "https://appwrite.io/docs/advanced/self-hosting/production/security", + "saa37bb5d4d7e": "https://appwrite.io/docs/partners/project/api-keys", + "sa0bd132f7968": "https://appwrite.io/docs/advanced/security/permissions", + "s58ad28816c5d": "https://appwrite.io/docs/products/databases/tablesdb/legacy/permissions", + "s2bf9a4746d28": "https://appwrite.io/docs/advanced/self-hosting/configuration/environment-variables", + "s000d05106450": "https://appwrite.io/docs/advanced/security/rate-limits", + "s38a1efa0c7d2": "https://appwrite.io/docs/advanced/self-hosting/configuration/email", + "s256ef73f7b85": "https://appwrite.io/docs/advanced/self-hosting/production/backups", + "s221c6aaef8df": "https://appwrite.io/docs/advanced/security/mfa" + } + }, + "utopia-php-database": { + "name": "utopia-php/database", + "basis": "7.3.9", + "sources": { + "s847e0ded0300": "https://github.com/utopia-php/database/blob/54364c687d6ddb3d976d7130b9001c0b29276933/src/Database/Helpers/Permission.php#L151-L155" + } + }, + "curl": { + "name": "curl", + "basis": "curl-8_14_1", + "sources": { + "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", + "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md", + "s82a34ee18632": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/config.md" + } + } + }, + "claims": { + "pb-bootstrap": {"text": "Privately bootstrap through the logged installer URL; superuser create EMAIL PASS exposes passwords in argv. Ordinary registration does not grant superuser rights.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:saf684a26056b"], "status": "REASONED"}, + "pb-installer": {"text": "Installer system-superuser token lasts about 30 minutes; protect its URL and startup logs as credentials.", "components": ["pb"], "sources": ["pb:saf684a26056b"], "status": "REASONED"}, + "pb-ips": {"text": "Superuser IP allowlists are available from v0.38.0; superuserIPs defaults empty and restricts authenticated requests, not /_/ static assets.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s5b66ca69bf6d"], "status": "REASONED"}, + "pb-tls": {"text": "serve with a domain provides native HTTPS/ACME; alternatively terminate TLS at a reverse proxy.", "components": ["pb"], "sources": ["pb:sb2129128b86c"], "status": "REASONED"}, + "pb-bind": {"text": "Without a domain serve defaults to 127.0.0.1:8090; with a domain it uses 0.0.0.0:80/443. Behind a local proxy omit the domain and set --http loopback.", "components": ["pb"], "sources": ["pb:s4b155ef30262"], "status": "REASONED"}, + "pb-origins": {"text": "--origins defaults * and accepts comma-separated allowed origins; CORS does not authorize records.", "components": ["pb"], "sources": ["pb:s4b155ef30262"], "status": "REASONED"}, + "pb-proxy": {"text": "Trust only headers the proxy overwrites, review useLeftmostIP ordering and block direct backend access to protect IP/rate controls.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a"], "status": "REASONED"}, + "pb-dev": {"text": "--dev adds diagnostics including SQL on stderr, not an auth bypass; its configured default comes from New(): true if os.Args[0] starts with a nonempty os.TempDir() or cacheDir() prefix, false otherwise.", "components": ["pb"], "sources": ["pb:seb56d3763223", "pb:s1b04c2cd16ec", "pb:sb63f49346abe", "pb:s2c8f78f2d5ac", "pb:sd700e81880f2", "pb:s5b0dd2fd7a6a", "pb:sf7c2fb5e78e7"], "status": "REASONED"}, + "pb-cli": {"text": "In the pinned CLI wrapper, Start() registers the serve and superuser commands on the root command and executes it.", "components": ["pb"], "sources": ["pb:s54430c83a38a"], "status": "REASONED"}, + "pb-rules": {"text": "Each list/view/create/update/delete rule defaults null (superuser-only); empty opens to guests and nonempty filters. Superusers bypass rules.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:saedbdfbdfc48", "pb:s04ae861b5709"], "status": "REASONED"}, + "pb-manage": {"text": "Top-level manageRule defaults null and permits privileged auth-record changes alongside create/update rules; its validator rejects empty-string rules.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-files": {"text": "File fields default unprotected despite locked record rules; Protected plus an authorized viewRule gates downloads, while a public viewRule still permits public access.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sf711b6345643", "pb:sa5e571f8d473"], "status": "REASONED"}, + "pb-file-token": {"text": "Short-lived file tokens provide identity context, not file authorization; viewRule decides. Protect token-bearing file URLs.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sf711b6345643", "pb:sa5e571f8d473"], "status": "REASONED"}, + "pb-realtime": {"text": "Collection subscriptions use listRule and individual-record subscriptions viewRule; SSE/subscription success does not prove event authorization.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sb850a71fad4d", "pb:sfd2f779e8e11"], "status": "REASONED"}, + "pb-password": {"text": "Auth initialization enables password authentication with email as the identity field.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-oauth": {"text": "OAuth2 defaults disabled and is unsupported for _superusers.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, + "pb-mfa-default": {"text": "MFA defaults disabled with duration 600 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-otp-default": {"text": "OTP defaults disabled with duration 180 seconds and length 8.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-alerts": {"text": "Authentication alerts default enabled; retain alerts and provide working email.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-auth-token": {"text": "Auth token duration defaults to 432000 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-file-duration": {"text": "File token duration defaults to 180 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-auth-rule": {"text": "authRule defaults empty; verified-only collections can require verified = true after email verification works.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, + "pb-mfa": {"text": "For superusers retain password auth and enable both OTP and MFA; empty mfa.rule applies to everyone. OTP alone is not a second factor.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, + "pb-logout": {"text": "Stateless authentication means clearing pb.authStore removes only the local copy, not a stolen token.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s773e441dd51c", "pb:s04ae861b5709"], "status": "REASONED"}, + "pb-encryption": {"text": "Settings default to plaintext JSON; select a 32-character environment secret with --encryptionEnv. This encrypts settings, not the whole database or files.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a"], "status": "REASONED"}, + "pb-backups": {"text": "Backups default local with empty cron (automatic backups off); archives contain main and logs databases plus remaining pb_data, excluding the backups directory and tracked exclusions; uploaded collection files on S3 require manual backup. Schedule/retain deliberately and protect archives.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a", "pb:se74188862a55"], "status": "REASONED"}, + "pb-backup-auth": {"text": "Backup downloads require a superuser file token and obey superuser IP restrictions; ordinary auth/file tokens do not suffice.", "components": ["pb"], "sources": ["pb:s31a5adccd695"], "status": "REASONED"}, + "pb-limiter": {"text": "Native limits exist from v0.23.0 but default disabled at v0.40.4; enable rateLimits.enabled.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a"], "status": "REASONED"}, + "pb-limit-rules": {"text": "Seeded limits are *:auth 2/3s, *:create 20/5s, /api/batch 3/1s and /api/ 300/10s.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a"], "status": "REASONED"}, + "pb-limit-bypass": {"text": "Superusers and rateLimits.excludedIPs bypass limits; test ordinary non-excluded clients.", "components": ["pb"], "sources": ["pb:sf6ffdca4dd39"], "status": "REASONED"}, + "pb-smtp": {"text": "smtp.enabled and smtp.tls default false; disabled SMTP falls back to sendmail. Configure protected transport and test actual delivery.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a", "pb:seb56d3763223"], "status": "REASONED"}, + "appwrite-https": {"text": "API FORCE_HTTPS and function/site ROUTER_FORCE_HTTPS default disabled at 2.2.0; explicitly enable both despite conflicting rolling deprecation/default documentation.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s427bab3733b6", "appwrite-docs:s2bf9a4746d28"], "status": "REASONED"}, + "appwrite-hosts": {"text": "ROUTER_PROTECTION defaults disabled; configure actual domains and enable rejection of unknown hostnames.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s427bab3733b6"], "status": "REASONED"}, + "appwrite-registration": {"text": "Console root-only registration defaults enabled with empty email/IP allowlists; these restrict account creation, not dashboard reachability or login.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s2bb21218c5ee"], "status": "REASONED"}, + "appwrite-console": {"text": "Claim the first operator privately and use a separate network/access boundary for a private dashboard.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s4a965bdeeb98", "appwrite:s2bb21218c5ee"], "status": "REASONED"}, + "appwrite-mfa": {"text": "Enroll and verify operator TOTP, protect recovery codes; at 2.2.0 enabled MFA with a verified factor requires a second session factor.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s221c6aaef8df", "appwrite:s193bfa5c6e22"], "status": "REASONED"}, + "appwrite-dev-env": {"text": "Development env disables root-only registration, abuse and router protection and carries placeholders; do not inherit it for production.", "components": ["appwrite"], "sources": ["appwrite:sadb8c6e7b00a"], "status": "REASONED"}, + "appwrite-recreate": {"text": "Apply env/Compose changes with docker compose up -d from the install directory, then check effective behavior.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s4a965bdeeb98"], "status": "REASONED"}, + "appwrite-permissions": {"text": "Collection and document grants are additive according to legacy documentation; documentSecurity defaults false in Appwrite 2.2.0 and must be enabled for document grants. The pinned parameter description states the need for one permission level.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:sf916851a0b10", "appwrite-docs:s58ad28816c5d"], "status": "REASONED"}, + "appwrite-creation": {"text": "For null or omitted document permissions, the pinned aggregate helper preserves null before the handler assigns read/update/delete user grants to a nonprivileged requester with a user ID, or an empty list otherwise. API-key classification skips role validation but does not itself determine the default list. Use specific users/teams for private data.", "components": ["appwrite", "appwrite-docs", "utopia-php-database"], "sources": ["appwrite:s5b4b3eb2271f", "appwrite:s93647e16b966", "appwrite:sda698c40fc34", "appwrite-docs:sa0bd132f7968", "appwrite:s7ba6f9c71972", "utopia-php-database:s847e0ded0300"], "status": "REASONED"}, + "appwrite-read-path": {"text": "Document GET classifies the requester from authorization roles (keys; owner/developer/admin privileged), reports disabled databases/collections as not found except to those requesters, and fetches the document through the database layer; collection permissions and documentSecurity are passed to the database layer at collection creation.", "components": ["appwrite"], "sources": ["appwrite:s5396e119c026", "appwrite:s82988de645fa", "appwrite:s224cc9d8a387", "appwrite:s1fe0e6185518"], "status": "REASONED"}, + "appwrite-requester": {"text": "With an API key the request role and scopes come from the key; a standard or ephemeral project key for the same project disables the default authorization status, as do admin-mode console sessions outside console project routes, while route scope validation applies to every requester.", "components": ["appwrite"], "sources": ["appwrite:sc55465cf878d", "appwrite:sed1bf9453ecb", "appwrite:s8784d02c82c9", "appwrite:sf60a2b283b80"], "status": "REASONED"}, + "appwrite-keys": {"text": "Properly scoped server API keys bypass resource permissions but still obey operation scopes; keys.write is admin-equivalent and keys never belong in clients.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s193bfa5c6e22", "appwrite-docs:saa37bb5d4d7e"], "status": "REASONED"}, + "appwrite-encryption": {"text": "Replace _APP_OPENSSL_KEY_V1=your-secret-key before production data; preserve the exact key separately, since replacement/loss strands encrypted secrets.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, + "appwrite-backups": {"text": "Back up database, persistent storage and deployment configuration; restrict access and test restoration in a separate installation.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, + "appwrite-abuse": {"text": "_APP_OPTIONS_ABUSE defaults enabled but dev env disables it; server API-key requests are exempt, so test with ordinary clients.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:sadb8c6e7b00a", "appwrite:s193bfa5c6e22", "appwrite-docs:s000d05106450"], "status": "REASONED"}, + "appwrite-smtp": {"text": "SMTP host/port/secure/username/password default empty and empty host disables sending; configure the provider and confirm message receipt/use.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite-docs:s38a1efa0c7d2"], "status": "REASONED"}, + "appwrite-storage": {"text": "Storage defaults local, upload limit 30000000 and server antivirus disabled; enabling scanning requires configured reachable ClamAV.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14"], "status": "REASONED"}, + "appwrite-bucket": {"text": "New bucket permissions are empty and fileSecurity=false; enable per-file security deliberately, since bucket grants also permit access.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3"], "status": "REASONED"}, + "appwrite-bucket-options": {"text": "Bucket encryption and antivirus default true; bucket antivirus does not enable the server scanner.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3"], "status": "REASONED"}, + "appwrite-size": {"text": "Encryption/scanning skip files above 20000000 bytes; cap maximumFileSize at that threshold when either is mandatory and test the boundary.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3", "appwrite:s4ab95ecbf6b5"], "status": "REASONED"}, + "appwrite-functions": {"text": "Function execute roles and execution-key scopes default empty; grant narrowly. Disabled functions still admit authorized Server SDK API keys.", "components": ["appwrite"], "sources": ["appwrite:s655a0abb9ef0"], "status": "REASONED"}, + "appwrite-executor": {"text": "Keep executor/orchestrator private, protect Docker-socket authority and replace _APP_EXECUTOR_SECRET; container execution is not demonstrated hostile-tenant isolation.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s8e5272615f2e", "appwrite-docs:s2bf9a4746d28"], "status": "REASONED"}, + "verify-pb-records": {"text": "Public rules allow guest reads; locked rules deny with 403, unsatisfied list filters can return empty 200 and view rules 404. Retain owner and separate superuser controls.", "components": ["pb-docs", "curl"], "sources": ["pb-docs:saedbdfbdfc48", "curl:s5e1383feef7d", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, + "verify-appwrite-records": {"text": "Broad collection grants defeat document restrictions; removing them must deny unrelated users while document grantees succeed. Compare client/server creation defaults.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s58ad28816c5d", "appwrite-docs:sa0bd132f7968"], "status": "REASONED", "verify": [1]}, + "verify-appwrite-keys": {"text": "A correctly scoped server key reads despite empty permissions, while a key without scope fails; neither substitutes for an ordinary-user control.", "components": ["appwrite"], "sources": ["appwrite:s193bfa5c6e22"], "status": "REASONED", "verify": [1]}, + "verify-registration": {"text": "On disposable Appwrite fixtures compare open signup 201 with root-only account-limit failure and independent email/IP restrictions; retain login/invitation controls.", "components": ["appwrite"], "sources": ["appwrite:s2bb21218c5ee"], "status": "REASONED", "verify": [2]}, + "verify-pb-bootstrap": {"text": "Guest and ordinary-user _superusers record creation must fail while a valid installer token creates the private operator; static dashboard assets prove no admin access.", "components": ["pb"], "sources": ["pb:saf684a26056b", "pb:s69aa223f52e4"], "status": "REASONED"}, + "verify-appwrite-mfa": {"text": "Console account GET succeeds without MFA, fails with user_more_factors_required for a fresh password-only MFA session, and succeeds after TOTP completion.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s221c6aaef8df", "appwrite:s193bfa5c6e22"], "status": "REASONED"}, + "verify-pb-writes": {"text": "Compare intended/unauthorized POST/PATCH/DELETE and privileged auth-record management; locked manageRule grants no management and empty-string configuration must fail.", "components": ["pb"], "sources": ["pb:s69aa223f52e4", "pb:sce114368c2ad"], "status": "REASONED"}, + "verify-pb-files": {"text": "Before protection unsigned file GET works; Protected plus private viewRule rejects guests/unrelated-user file tokens with 404 while permitted-user bytes still arrive.", "components": ["pb", "curl"], "sources": ["pb:sa5e571f8d473", "curl:s82a34ee18632"], "status": "REASONED"}, + "verify-pb-realtime": {"text": "Mutate a fixture while comparing collection and record subscriptions; deny unauthorized event delivery, retaining authorized delivery. PB_CONNECT/acknowledgment alone is insufficient.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sb850a71fad4d", "pb:sfd2f779e8e11"], "status": "REASONED"}, + "verify-pb-mfa": {"text": "Compare password-only completion with password-plus-OTP MFA and verified/unverified authRule logins; clearing authStore must not revoke a retained valid token.", "components": ["pb-docs"], "sources": ["pb-docs:s773e441dd51c"], "status": "REASONED"}, + "verify-pb-ips": {"text": "Compare allowed/excluded superuser sources and forged forwarded headers; excluded sources must get 403 and direct backend access must fail with a working proxy control.", "components": ["pb"], "sources": ["pb:s5b66ca69bf6d", "pb:s03dd7dd84c2a"], "status": "REASONED"}, + "verify-transport": {"text": "Compare HTTP/HTTPS and certificates, Appwrite API/function/site domains and configured/unknown Host; compare browser origins separately from record authorization.", "components": ["pb", "appwrite"], "sources": ["pb:s4b155ef30262", "appwrite:s427bab3733b6"], "status": "REASONED"}, + "verify-abuse": {"text": "Cross each configured threshold with ordinary non-excluded clients and compare disabled/enabled controls; PocketBase limiting yields 429 with later successful recovery.", "components": ["pb", "appwrite-docs"], "sources": ["pb:sf6ffdca4dd39", "appwrite-docs:s000d05106450"], "status": "REASONED"}, + "verify-email": {"text": "Request OTP, verification and recovery messages and confirm receipt and use; request success alone is not delivery evidence.", "components": ["pb", "appwrite-docs"], "sources": ["pb:seb56d3763223", "appwrite-docs:s38a1efa0c7d2"], "status": "REASONED"}, + "verify-pb-recovery": {"text": "Compare plaintext/encrypted persisted settings, restart with retained key and restore local/S3 data; only permitted superuser file tokens may download backups.", "components": ["pb", "pb-docs"], "sources": ["pb:s03dd7dd84c2a", "pb:s31a5adccd695", "pb-docs:s9029ab974cf1"], "status": "REASONED"}, + "verify-appwrite-recovery": {"text": "Restore encrypted fixtures with the original key; a separate wrong-key fixture must not recover encrypted values.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, + "verify-uploads": {"text": "Compare permitted/unrelated uploads and size-boundary fixtures; mandatory processing requires rejecting over-limit files and confirming accepted-file encryption/scanning.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3", "appwrite:s4ab95ecbf6b5"], "status": "REASONED"}, + "verify-functions": {"text": "Compare intended execute roles, unrelated users and privileged server keys, including disabled functions; confirm executor/orchestrator privacy from a second host.", "components": ["appwrite"], "sources": ["appwrite:s655a0abb9ef0", "appwrite:s8e5272615f2e"], "status": "REASONED"}, + "verify-bundle": {"text": "Search filenames using protected exact-secret patterns with a disposable marker control; client bundles must contain neither superuser tokens nor Appwrite API keys, and encoded/split forms can evade scanning.", "components": ["pb-docs", "appwrite-docs"], "sources": ["pb-docs:s773e441dd51c", "appwrite-docs:saa37bb5d4d7e"], "status": "REASONED", "verify": [3]}, + "local-guards": {"text": "Recorded guard tests rejected placeholders and omitted/shortened arguments; this demonstrates shell behavior only, not service access or deployed-bundle scanning.", "components": ["pb-docs", "appwrite-docs"], "sources": ["pb-docs:saedbdfbdfc48", "appwrite-docs:sa0bd132f7968", "appwrite-docs:saa37bb5d4d7e"], "status": "DEMONSTRATED", "evidence": "Local guard tests refused embedded `REPLACE_WITH_` placeholders, `example.com`, angle brackets, empty arguments, and omitted or shortened `set --` lines."}, + "local-syntax": {"text": "All three bash fences have recorded local lint/syntax success, not live service evidence; ShellCheck version is recorded only in the body.", "components": ["appwrite", "pb-docs", "appwrite-docs"], "sources": ["pb-docs:saedbdfbdfc48", "appwrite:s2bb21218c5ee", "appwrite-docs:saa37bb5d4d7e"], "status": "DEMONSTRATED", "evidence": "All three bash blocks passed ShellCheck 0.11.0 and `bash -n` during authoring."}, + "source-limits": {"text": "The PocketBase CLI wrapper, --dev default, and Appwrite requester classification and document-read call sites are now cited; how utopia-php/database matches roles against collection/document permissions (including documentSecurity) and what the disabled default authorization status bypasses remain untraced in that unfetched dependency.", "components": ["appwrite-docs", "pb-docs", "appwrite"], "sources": ["appwrite-docs:s58ad28816c5d", "pb-docs:s9029ab974cf1", "appwrite:s7ba6f9c71972"], "status": "REASONED"} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/ray.md b/plugin/skills/secure-deployment-config/references/ray.md index 2097d9418..acb300bdc 100644 --- a/plugin/skills/secure-deployment-config/references/ray.md +++ b/plugin/skills/secure-deployment-config/references/ray.md @@ -1,317 +1,18 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-26", - "documentation_checked": "2026-09", - "body_sha256": "286cb3508e87ad0658bd57c73270ab3b4100681250bae339f4d71a573e537bfb", - "components": { - "ray": { - "name": "Ray pinned source", - "basis": "ray-2.58.0", - "sources": { - "sed3f197cc1ef": "https://github.com/ray-project/ray/blob/ray-2.58.0/doc/source/ray-security/token-auth.md", - "s08d31327265a": "https://raw.githubusercontent.com/ray-project/ray/ray-2.58.0/docker/base-deps/Dockerfile", - "s2740a1880e2b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/rpc/grpc_server.cc#L67-L68", - "s958054ab801b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.h#L111-L113", - "sa3d2966f536b": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L601-L617", - "s050b349f5bbd": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L185", - "sd7d2d0d963d0": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.cc#L257-L278", - "s329ab4e0ed63": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/serve/_private/replica.py#L1778", - "s3eb2c2cb2cbb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/util/client/server/server.py#L799-L801", - "sae1bb5af6e35": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/util/client/server/proxier.py#L929-L931", - "s207ba00c4e86": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1572-L1575", - "s3efda42d7913": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L2464-L2471", - "s9a060fe08a53": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L189", - "sb1935b32e677": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L618-L635", - "se9af8ec90932": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L687-L692", - "s17bc66b5543e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L20-L23", - "s7df4fce455ca": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L54-L66", - "sc96f32b8a459": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L107-L113", - "sbca5ef4de0bb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/authentication/http_token_authentication.py#L28-L80", - "scdfff3ba79f6": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/optional_utils.py#L132-L209", - "s528944bdec23": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_agent.py#L32-L196", - "s4796e2ca38da": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_head.py#L123-L144", - "s1d86b2f31870": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_head.py#L267-L280", - "s9976ecf5b1f1": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L245-L249", - "sa542601368d1": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L264-L307", - "sd12bacfbc3c7": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L345-L414", - "s7ef1edbcda5c": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L570-L638", - "s6f5310eb313d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L144-L169", - "s8f508c7e2c94": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L182-L187", - "s48b3469099a3": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L100-L117", - "s532a98227d6d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L372-L377", - "s33a5ff7c987b": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1660-L1669", - "s1a19fd793b8f": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1768", - "s8f1ebe4bd911": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1381-L1387", - "s7682a13e83a2": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1862-L1883", - "s9237e74ea054": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1918-L1929", - "s4ce6fe077ba9": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L2022-L2031", - "s7f27125f73b7": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L504-L516", - "sf921a5d6a898": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/prometheus_exporter.py#L326-L334", - "s434c7b4364dc": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L218-L224", - "s08aa128e5d70": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L243-L248", - "sdd8960062a9c": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/includes/network_util.pxi#L103-L110", - "s51476b743c19": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.cc#L280-L289", - "s7451c8e00be3": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L850-L854", - "s6c7faadf88a4": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L786-L821", - "s3625ec280c5e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/worker.py#L1835-L1836", - "sd3a18ab2a112": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L507-L514", - "sba3f67581471": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/parameter.py#L165-L167", - "s994d0ddb6a7d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/worker.py#L1884-L1912", - "sd4d44da36278": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/utils.py#L1128-L1142", - "s5a9d9c171f49": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1909-L1913", - "s9cd95d66d32a": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/utils.py#L322-L351", - "s762672f680e9": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L2099-L2101", - "s28e3a1076d01": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L23-L33", - "s883e3e00e4df": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/common/ray_config_def.h#L618-L619", - "sdb1c4564137b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/raylet/node_manager.cc#L3527-L3530", - "sa7d4f677aadb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_agent.py#L198-L203", - "sa5873642c8a2": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_manager.py#L421-L463", - "s2d562f0e7567": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_manager.py#L565-L609", - "s3a079f7e3d82": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/prometheus_exporter.py#L10", - "sefae1a66c0dd": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L102-L114", - "scc7af6be456e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/serve/config.py#L955-L963" - } - }, - "docs": { - "name": "Ray documentation", - "basis": "unknown", - "sources": { - "sa5e2c2d58c12": "https://docs.ray.io/en/latest/ray-security/index.html", - "s6f0aa11f549a": "https://docs.ray.io/en/latest/ray-security/token-auth.html", - "s56c7c98ab722": "https://docs.ray.io/en/latest/cluster/cli.html", - "s261570b2672b": "https://docs.ray.io/en/latest/ray-core/configure.html", - "sc375ff2ca01f": "https://docs.ray.io/en/latest/ray-core/handling-dependencies.html", - "sb7a1d0b7dfb3": "https://docs.ray.io/en/latest/ray-core/api/doc/ray.runtime_env.RuntimeEnv.html", - "scd3f7d8a9b02": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.config.HTTPOptions.html", - "s15aa6ec80fb8": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.schema.HTTPOptionsSchema.html", - "s045d75fbdecc": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.schema.ServeDeploySchema.html", - "sc98a37c2b157": "https://docs.ray.io/en/latest/serve/http-guide.html", - "s54ed4b890b65": "https://docs.ray.io/en/latest/cluster/kubernetes/user-guides/kuberay-gcs-ft.html" - } - }, - "token-min": { - "name": "Ray token authentication introduction", - "basis": "2.52.0", - "sources": { - "sa5e2c2d58c12": "https://docs.ray.io/en/latest/ray-security/index.html" - } - }, - "redis-history": { - "name": "Ray Redis history announcement", - "basis": "unknown", - "sources": { - "sb5afda59324d": "https://www.anyscale.com/blog/redis-in-ray-past-and-future" - } - }, - "docker": { - "name": "Docker publishing documentation", - "basis": "unknown", - "sources": { - "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" - } - }, - "kuberay-auth": { - "name": "KubeRay authentication documentation", - "basis": "unknown", - "sources": { - "saf5247262bc7": "https://docs.ray.io/en/latest/cluster/kubernetes/user-guides/kuberay-auth.html" - } - }, - "kuberay": { - "name": "KubeRay chart", - "basis": "v1.7.0", - "sources": { - "s76e45858aa01": "https://github.com/ray-project/kuberay/blob/v1.7.0/helm-chart/ray-cluster/values.yaml" - } - }, - "kubernetes": { - "name": "Kubernetes security contexts", - "basis": "unknown", - "sources": { - "sb78ea91d0308": "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/" - } - }, - "python": { - "name": "CPython WSGI server", - "basis": "v3.11.0", - "sources": { - "sdd5a8e2ab59c": "https://github.com/python/cpython/blob/v3.11.0/Lib/wsgiref/simple_server.py#L1-L160", - "se4933ce3da65": "https://github.com/python/cpython/blob/v3.11.0/Lib/http/server.py#L120-L145", - "s0020052d7267": "https://github.com/python/cpython/blob/v3.11.0/Lib/socketserver.py#L400-L480" - } - }, - "curl": { - "name": "curl minimum write-out version", - "basis": "7.75.0", - "sources": { - "s2b2686afaf41": "https://curl.se/docs/manpage.html" - } - }, - "docker-release": { - "name": "Docker Engine release notes", - "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", - "sources": { - "sd733f57ded23": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/release-notes/28.md#L768-L768" - } - }, - "kuberay-auth-source": { - "name": "KubeRay authentication source", - "basis": "v1.6.0", - "sources": { - "s9a9f9edcc07e": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/apis/ray/v1/raycluster_types.go#L17-L19", - "s51c84c0bc3b4": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/apis/ray/v1/raycluster_types.go#L81-L107", - "s61e750cdc2e0": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/raycluster_controller.go#L369-L433", - "s6171707b3edb": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L194-L220", - "s408a3a202322": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L234-L236", - "s8505add03e5a": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L253-L271", - "s1cb4f972131a": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L296-L336", - "sd1b672bc4edd": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L460-L462", - "se37635162dc9": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/utils/util.go#L760-L766" - } - }, - "kuberay-auth-prior": { - "name": "KubeRay prior RayCluster types", - "basis": "v1.5.0", - "sources": { - "s04c5ce0b599d": "https://github.com/ray-project/kuberay/blob/v1.5.0/ray-operator/apis/ray/v1/raycluster_types.go#L12-L47" - } - } - }, - "claims": { - "boundary": {"text": "Dashboard, Jobs and Client access permits arbitrary code execution; use an isolated cluster network and separate clusters for mutually untrusted jobs.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12"], "status": "REASONED"}, - "dashboard": {"text": "Dashboard defaults to resolved localhost, IPv4 before IPv6 then 127.0.0.1, on port 8265; state loopback explicitly.", "components": ["ray"], "sources": ["ray:sa3d2966f536b", "ray:s050b349f5bbd", "ray:sd7d2d0d963d0"], "status": "REASONED"}, - "docker": {"text": "Bridge publishing uses host loopback and container-interface bind; this guide assesses Docker 28.0.0 or later with normal NAT as the loopback-isolation minimum because the 28.0.0 release notes record a fix for neighbour hosts reaching loopback-mapped ports.", "components": ["docker", "docker-release"], "sources": ["docker:s1e53417c513d", "docker-release:sd733f57ded23"], "status": "REASONED"}, - "access": {"text": "Reach Dashboard/Jobs through loopback SSH forwarding, launcher forwarding, KubeRay forwarding or a private tailnet; public browser access needs an authenticating TLS proxy.", "components": ["docs", "kuberay-auth"], "sources": ["docs:sa5e2c2d58c12", "docs:s56c7c98ab722", "kuberay-auth:saf5247262bc7"], "status": "REASONED"}, - "head-ports": {"text": "Head GCS uses 6379 and Client 10001; worker ports default 10002-19999 plus randomized listeners.", "components": ["docs"], "sources": ["docs:s56c7c98ab722", "docs:s261570b2672b"], "status": "REASONED"}, - "core-bind": {"text": "Core gRPC source binds all interfaces unless node address is exactly 127.0.0.1, ::1 or localhost; private node IP does not imply private bind.", "components": ["ray"], "sources": ["ray:s2740a1880e2b", "ray:s958054ab801b"], "status": "REASONED"}, - "gcs-observation": {"text": "Four attempted Ray 2.58.0 loopback starts still showed GCS *:6379; the cause was not established and the watcher stopped the runs.", "components": ["ray"], "sources": ["ray:s2740a1880e2b", "ray:s958054ab801b"], "status": "DEMONSTRATED", "evidence": "in each of four runs using `ray start --head --node-ip-address=127.0.0.1 --port=6379 --dashboard-host=127.0.0.1`, `ss` still showed the GCS listener as `*:6379`."}, - "client-bind": {"text": "Ray Client server/proxier listen on the supplied node address plus loopback; restrict access and prefer Jobs over publishing Client.", "components": ["ray"], "sources": ["ray:s3eb2c2cb2cbb", "ray:sae1bb5af6e35", "ray:s207ba00c4e86", "ray:s3efda42d7913"], "status": "REASONED"}, - "serve-replica": {"text": "Serve inter-deployment gRPC binds [::] on an OS-chosen port; this is source reasoning, not a run.", "components": ["ray"], "sources": ["ray:s329ab4e0ed63"], "status": "REASONED"}, - "agents": {"text": "--include-dashboard=false does not stop dashboard/runtime-env agents; head dashboard itself is reduced to usage stats.", "components": ["ray"], "sources": ["ray:s33a5ff7c987b", "ray:s1a19fd793b8f", "ray:s8f1ebe4bd911", "ray:s7682a13e83a2", "ray:s9237e74ea054", "ray:s4ce6fe077ba9"], "status": "REASONED"}, - "minimal": {"text": "Missing optional imports select minimal mode: no agent HTTP/gRPC or metrics reporter; runtime-env HTTP remains. Base installs may have full dependencies.", "components": ["ray"], "sources": ["ray:s48b3469099a3", "ray:sd4d44da36278", "ray:s5a9d9c171f49", "ray:s9cd95d66d32a", "ray:s762672f680e9", "ray:s28e3a1076d01"], "status": "REASONED"}, - "agent-http": {"text": "Non-minimal agent HTTP defaults 52365, including ray.init; --dashboard-agent-listen-port changes it, with node-IP plus conditional localhost binds.", "components": ["ray"], "sources": ["ray:s9a060fe08a53", "ray:sb1935b32e677", "ray:s7df4fce455ca", "ray:sba3f67581471", "ray:s994d0ddb6a7d"], "status": "REASONED"}, - "agent-jobs": {"text": "Direct /api/job_agent/jobs/ submission and stop/delete/log routes bypass a proxy on 8265; head forwarding reaches its agent job manager.", "components": ["ray"], "sources": ["ray:s528944bdec23", "ray:s4796e2ca38da", "ray:s1d86b2f31870"], "status": "REASONED"}, - "agent-placement": {"text": "Worker agents accept cluster submissions with their own managers; drivers normally use the head unless resources, labels or worker-placement override permit workers.", "components": ["ray"], "sources": ["ray:sa7d4f677aadb", "ray:sa5873642c8a2", "ray:s2d562f0e7567"], "status": "REASONED"}, - "agent-logs": {"text": "Agent static /logs exposes the node log directory and index.", "components": ["ray"], "sources": ["ray:s9976ecf5b1f1"], "status": "REASONED"}, - "agent-http-auth": {"text": "Without token mode a browser heuristic is the only filter; token mode guards all agent HTTP paths except two health endpoints.", "components": ["ray"], "sources": ["ray:s17bc66b5543e", "ray:sc96f32b8a459", "ray:sbca5ef4de0bb", "ray:scdfff3ba79f6"], "status": "REASONED"}, - "agent-grpc": {"text": "Agent gRPC defaults OS-assigned with --dashboard-agent-grpc-port override; localhost or wildcard bind serves log/profiling RPCs with optional token interception.", "components": ["ray"], "sources": ["ray:sb1935b32e677", "ray:sa542601368d1", "ray:sd12bacfbc3c7", "ray:s7ef1edbcda5c", "ray:s6f5310eb313d", "ray:s8f508c7e2c94", "ray:s532a98227d6d"], "status": "REASONED"}, - "runtime-agent": {"text": "Runtime-env HTTP defaults OS-assigned on node IP with --runtime-env-agent-port override; create/delete installs packages, and token middleware exempts no route.", "components": ["ray"], "sources": ["ray:s532a98227d6d", "ray:s434c7b4364dc", "ray:s08aa128e5d70", "ray:sb1935b32e677"], "status": "REASONED"}, - "metrics-agent": {"text": "Metrics default enabled; non-minimal reporter uses an OS-assigned port or --metrics-export-port, outside all token middleware.", "components": ["ray"], "sources": ["ray:s532a98227d6d", "ray:s7f27125f73b7", "ray:sf921a5d6a898", "ray:s883e3e00e4df", "ray:sdb1c4564137b", "ray:se9af8ec90932"], "status": "REASONED"}, - "metrics-ipv4": {"text": "WSGI metrics uses AF_INET: empty address is IPv4 wildcard, localhost is used for local node addresses, and resolved ::1 cannot bind this socket.", "components": ["ray", "python"], "sources": ["ray:s7f27125f73b7", "ray:sf921a5d6a898", "ray:s3a079f7e3d82", "python:sdd5a8e2ab59c", "python:se4933ce3da65", "python:s0020052d7267"], "status": "REASONED"}, - "node-normalization": {"text": "Both CLI and ray.init normalize localhost node arguments to a discovered address; cluster-mode setting changes whole-node selection, not independent agent binds.", "components": ["ray"], "sources": ["ray:s7451c8e00be3", "ray:s6c7faadf88a4", "ray:s3625ec280c5e", "ray:sd3a18ab2a112"], "status": "REASONED"}, - "agent-family": {"text": "Agent gRPC wildcard selection is 0.0.0.0 or :: based on resolved localhost, independently of node address family.", "components": ["ray"], "sources": ["ray:sdd8960062a9c", "ray:s51476b743c19"], "status": "REASONED"}, - "gcs-not-redis": {"text": "6379 is GCS, not Redis; Redis stopped being launched by default in 1.11.", "components": ["redis-history", "docs"], "sources": ["redis-history:sb5afda59324d", "docs:s56c7c98ab722"], "status": "REASONED"}, - "redis-backend": {"text": "External Redis is opt-in GCS fault-tolerance storage; Redis credentials do not authenticate GCS clients and RAY_REDIS_PASSWORD is not a head flag substitute.", "components": ["docs"], "sources": ["docs:s56c7c98ab722", "docs:s54ed4b890b65"], "status": "REASONED"}, - "token-default": {"text": "Shared token authentication exists since 2.52.0 but is disabled by default as of 2.58.0; it supplements network isolation and does not protect Serve apps.", "components": ["token-min", "ray"], "sources": ["token-min:sa5e2c2d58c12", "ray:sed3f197cc1ef"], "status": "REASONED"}, - "token-input": {"text": "Enable RAY_AUTH_MODE=token; precedence is RAY_AUTH_TOKEN, RAY_AUTH_TOKEN_PATH then ~/.ray/auth_token, shared by every node/client.", "components": ["ray"], "sources": ["ray:sed3f197cc1ef"], "status": "REASONED"}, - "token-storage": {"text": "Generate/protect token files and copy before startup; tokens are plaintext, do not expire and must never be committed.", "components": ["ray"], "sources": ["ray:sed3f197cc1ef"], "status": "REASONED"}, - "token-transport": {"text": "Token HTTP headers require a trusted tunnel or TLS; plaintext HTTP exposes them on the network.", "components": ["docs", "ray"], "sources": ["docs:s6f0aa11f549a", "ray:sed3f197cc1ef"], "status": "REASONED"}, - "kuberay-token": {"text": "KubeRay v1.6.0 defines authOptions; v1.5.0 RayClusterSpec lacks it. With token mode enabled, the controller creates a random-token Secret when Kubernetes ServiceAccount token authentication is off, SecretName is null or empty, and the generated-name Secret does not already exist; it leaves an existing generated-name Secret or a supplied named Secret in place and creates no Secret in ServiceAccount mode. The pod builder sets RAY_AUTH_MODE when unset on head and worker Ray containers, on the wait-gcs-ready init container when present, and on the head autoscaler when autoscaling is enabled; outside ServiceAccount mode it adds RAY_AUTH_TOKEN from the selected Secret when unset, and in ServiceAccount mode it does not add RAY_AUTH_TOKEN. This guide infers support in v1.6.0 and later from the compared tags; later releases have not been checked.", "components": ["kuberay-auth", "kuberay-auth-source", "kuberay-auth-prior"], "sources": ["kuberay-auth:saf5247262bc7", "kuberay-auth-source:s9a9f9edcc07e", "kuberay-auth-source:s51c84c0bc3b4", "kuberay-auth-source:s61e750cdc2e0", "kuberay-auth-source:s6171707b3edb", "kuberay-auth-source:s408a3a202322", "kuberay-auth-source:s8505add03e5a", "kuberay-auth-source:s1cb4f972131a", "kuberay-auth-source:sd1b672bc4edd", "kuberay-auth-source:se37635162dc9", "kuberay-auth-prior:s04c5ce0b599d"], "status": "REASONED"}, - "mfa": {"text": "Ray has no user accounts; MFA belongs on SSH, tailnet or identity-aware proxy access.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12"], "status": "REASONED"}, - "grpc-tls": {"text": "RAY_USE_TLS defaults 0; export it and server certificate/key/CA variables on every node before startup for internal gRPC mutual TLS.", "components": ["docs"], "sources": ["docs:s261570b2672b"], "status": "REASONED"}, - "tls-scope": {"text": "gRPC TLS can cost performance and does not replace isolation or the dashboard/Jobs HTTP TLS proxy.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12", "docs:s261570b2672b"], "status": "REASONED"}, - "runtime-code": {"text": "Job runtime_env installs before entrypoint; ray.init environments apply to child tasks/actors. Packages, archives, py_executable and setup hooks execute submitted code.", "components": ["docs"], "sources": ["docs:sc375ff2ca01f"], "status": "REASONED"}, - "runtime-policy": {"text": "No built-in package/URI/hook admission policy; eager_install and setup_timeout_seconds control timing, not trust. Prebuild reviewed dependencies.", "components": ["docs"], "sources": ["docs:sc375ff2ca01f", "docs:sb7a1d0b7dfb3"], "status": "REASONED"}, - "serve-bind": {"text": "Serve HTTP defaults port 8000, Python host 127.0.0.1 but config-file host 0.0.0.0; EveryNode puts proxies on nodes with replicas.", "components": ["docs"], "sources": ["docs:scd3f7d8a9b02", "docs:s15aa6ec80fb8", "docs:s045d75fbdecc"], "status": "REASONED"}, - "serve-auth": {"text": "Serve apps have no built-in application auth and are outside cluster token mode; restrict origins and add proxy or FastAPI authentication.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12", "docs:sc98a37c2b157"], "status": "REASONED"}, - "serve-tls": {"text": "Serve ssl_keyfile/ssl_certfile/ssl_ca_certs default None; supplying a CA does not itself require client certificates.", "components": ["docs"], "sources": ["docs:scd3f7d8a9b02"], "status": "REASONED"}, - "serve-grpc": {"text": "Optional Serve gRPC uses 9000 only with configured servicer functions.", "components": ["docs", "ray"], "sources": ["docs:s045d75fbdecc", "ray:scc7af6be456e"], "status": "REASONED"}, - "image-privilege": {"text": "Official image ray UID 1000/GID 100 has passwordless sudo; non-root startup alone does not prevent root escalation.", "components": ["ray"], "sources": ["ray:s08d31327265a"], "status": "REASONED"}, - "pod-security": {"text": "KubeRay v1.7.0 head/worker pod/container contexts default empty; enforce non-root UID/GID, no privilege escalation, dropped capabilities and RuntimeDefault seccomp.", "components": ["kuberay", "kubernetes"], "sources": ["kuberay:s76e45858aa01", "kubernetes:sb78ea91d0308"], "status": "REASONED"}, - "verify-inventory": {"text": "Inventory every head/worker and actual randomized agent ports after restarts; expected missing listeners need explanation. No isolated multi-node cluster was available.", "components": ["ray"], "sources": ["ray:s9a060fe08a53", "ray:s6f5310eb313d", "ray:s532a98227d6d", "ray:s7f27125f73b7", "ray:s434c7b4364dc"], "status": "REASONED", "verify": [1]}, - "verify-external": {"text": "Probe every routed IPv4/IPv6 origin and inventoried port; completed TCP, even HTTP 401 or non-HTTP, proves exposure. Resolver/local errors are inconclusive.", "components": ["docs", "curl"], "sources": ["docs:sa5e2c2d58c12", "docs:s261570b2672b", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, - "verify-jobs": {"text": "Through the trusted tunnel valid-token submission must succeed and token-free submission must fail authentication with 401, not a connection error.", "components": ["ray", "kuberay-auth"], "sources": ["ray:sed3f197cc1ef", "kuberay-auth:saf5247262bc7"], "status": "REASONED", "verify": [1]}, - "verify-grpc-tls": {"text": "ray health-check must pass with the correct CA and fail certificate verification with a wrong CA/name; local config errors are inconclusive.", "components": ["docs"], "sources": ["docs:s261570b2672b"], "status": "REASONED", "verify": [1]}, - "verify-agent": {"text": "Direct non-browser /logs/ must expose an index with token mode off, allow a valid token with it on and reject absent tokens with 401; health exemptions cannot discriminate.", "components": ["ray"], "sources": ["ray:s9976ecf5b1f1", "ray:s17bc66b5543e", "ray:sbca5ef4de0bb", "ray:sefae1a66c0dd"], "status": "REASONED", "verify": [2]}, - "verify-token-secret": {"text": "Trusted-path hidden prompt and stdin header avoid new token argv/history exposure and set +x avoids tracing; account owner/root can still read memory.", "components": ["ray", "curl"], "sources": ["ray:sed3f197cc1ef", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]} - } -} ---- # Ray: dashboard, Jobs, and Client ports execute code - -**Version basis** +Written against Ray pinned source ray-2.58.0, Ray token authentication introduction 2.52.0, KubeRay chart v1.7.0, KubeRay authentication source v1.6.0, KubeRay prior RayCluster types v1.5.0, CPython WSGI server v3.11.0, and curl minimum write-out version 7.75.0; 1 further source is pinned to a commit and 5 carry no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 46 claims: 1 demonstrated by a check that ran and 45 reasoned from cited sources. -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. -Metadata reviewed 2026-09-26; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +Ray's own security page is blunt: if you expose the Ray Dashboard, Ray Jobs, or Ray Client services, "anybody who can access the associated ports can execute arbitrary code on your Ray Cluster", explicitly by submitting a Job or connecting a Client, indirectly through the Dashboard REST API, and implicitly because Ray deserializes arbitrary Python objects with cloudpickle. Ray "doesn't implement access controls for developers interacting with a given cluster"; security and isolation "must be enforced outside of the Ray Cluster". The head ports in question are the dashboard (and Jobs API) on `8265`, the Ray Client server on `10001`, and the head node port `6379`, all plain HTTP or gRPC with no login of their own unless you enable token authentication (step 3). The per-node agent listeners in step 2 add independent paths to job submission, logs, profiling, runtime environments and metrics. -Components (recorded basis): +## 1. Keep the dashboard on loopback -- ray: Ray pinned source (ray-2.58.0) -- docs: Ray documentation (unknown) -- token-min: Ray token authentication introduction (2.52.0) -- redis-history: Ray Redis history announcement (unknown) -- docker: Docker publishing documentation (unknown) -- kuberay-auth: KubeRay authentication documentation (unknown) -- kuberay: KubeRay chart (v1.7.0) -- kubernetes: Kubernetes security contexts (unknown) -- python: CPython WSGI server (v3.11.0) -- curl: curl minimum write-out version (7.75.0) -- docker-release: Docker Engine release notes (4e9a5751518ed8223a8dcde53693badddd72604f) -- kuberay-auth-source: KubeRay authentication source (v1.6.0) -- kuberay-auth-prior: KubeRay prior RayCluster types (v1.5.0) +`ray start --dashboard-host` defaults to localhost (as of Ray 2.58.0, the first address `localhost` resolves to, IPv4 before IPv6, else `127.0.0.1`), and `--dashboard-port` defaults to `8265`. Leave both alone and say so explicitly, because most tutorials and container entrypoints override the host to make the UI reachable: -Claims (full source mapping in the front matter): - -- boundary (REASONED; docs): Dashboard, Jobs and Client access permits arbitrary code execution; use an isolated cluster network and separate clusters for mutually untrusted jobs. -- dashboard (REASONED; ray): Dashboard defaults to resolved localhost, IPv4 before IPv6 then 127.0.0.1, on port 8265; state loopback explicitly. -- docker (REASONED; docker, docker-release): Bridge publishing uses host loopback and container-interface bind; this guide assesses Docker 28.0.0 or later with normal NAT as the loopback-isolation minimum because the 28.0.0 release notes record a fix for neighbour hosts reaching loopback-mapped ports. -- access (REASONED; docs, kuberay-auth): Reach Dashboard/Jobs through loopback SSH forwarding, launcher forwarding, KubeRay forwarding or a private tailnet; public browser access needs an authenticating TLS proxy. -- head-ports (REASONED; docs): Head GCS uses 6379 and Client 10001; worker ports default 10002-19999 plus randomized listeners. -- core-bind (REASONED; ray): Core gRPC source binds all interfaces unless node address is exactly 127.0.0.1, ::1 or localhost; private node IP does not imply private bind. -- gcs-observation (DEMONSTRATED; ray): Four attempted Ray 2.58.0 loopback starts still showed GCS *:6379; the cause was not established and the watcher stopped the runs. -- client-bind (REASONED; ray): Ray Client server/proxier listen on the supplied node address plus loopback; restrict access and prefer Jobs over publishing Client. -- serve-replica (REASONED; ray): Serve inter-deployment gRPC binds [::] on an OS-chosen port; this is source reasoning, not a run. -- agents (REASONED; ray): --include-dashboard=false does not stop dashboard/runtime-env agents; head dashboard itself is reduced to usage stats. -- minimal (REASONED; ray): Missing optional imports select minimal mode: no agent HTTP/gRPC or metrics reporter; runtime-env HTTP remains. Base installs may have full dependencies. -- agent-http (REASONED; ray): Non-minimal agent HTTP defaults 52365, including ray.init; --dashboard-agent-listen-port changes it, with node-IP plus conditional localhost binds. -- agent-jobs (REASONED; ray): Direct /api/job_agent/jobs/ submission and stop/delete/log routes bypass a proxy on 8265; head forwarding reaches its agent job manager. -- agent-placement (REASONED; ray): Worker agents accept cluster submissions with their own managers; drivers normally use the head unless resources, labels or worker-placement override permit workers. -- agent-logs (REASONED; ray): Agent static /logs exposes the node log directory and index. -- agent-http-auth (REASONED; ray): Without token mode a browser heuristic is the only filter; token mode guards all agent HTTP paths except two health endpoints. -- agent-grpc (REASONED; ray): Agent gRPC defaults OS-assigned with --dashboard-agent-grpc-port override; localhost or wildcard bind serves log/profiling RPCs with optional token interception. -- runtime-agent (REASONED; ray): Runtime-env HTTP defaults OS-assigned on node IP with --runtime-env-agent-port override; create/delete installs packages, and token middleware exempts no route. -- metrics-agent (REASONED; ray): Metrics default enabled; non-minimal reporter uses an OS-assigned port or --metrics-export-port, outside all token middleware. -- metrics-ipv4 (REASONED; ray, python): WSGI metrics uses AF_INET: empty address is IPv4 wildcard, localhost is used for local node addresses, and resolved ::1 cannot bind this socket. -- node-normalization (REASONED; ray): Both CLI and ray.init normalize localhost node arguments to a discovered address; cluster-mode setting changes whole-node selection, not independent agent binds. -- agent-family (REASONED; ray): Agent gRPC wildcard selection is 0.0.0.0 or :: based on resolved localhost, independently of node address family. -- gcs-not-redis (REASONED; redis-history, docs): 6379 is GCS, not Redis; Redis stopped being launched by default in 1.11. -- redis-backend (REASONED; docs): External Redis is opt-in GCS fault-tolerance storage; Redis credentials do not authenticate GCS clients and RAY_REDIS_PASSWORD is not a head flag substitute. -- token-default (REASONED; token-min, ray): Shared token authentication exists since 2.52.0 but is disabled by default as of 2.58.0; it supplements network isolation and does not protect Serve apps. -- token-input (REASONED; ray): Enable RAY_AUTH_MODE=token; precedence is RAY_AUTH_TOKEN, RAY_AUTH_TOKEN_PATH then ~/.ray/auth_token, shared by every node/client. -- token-storage (REASONED; ray): Generate/protect token files and copy before startup; tokens are plaintext, do not expire and must never be committed. -- token-transport (REASONED; docs, ray): Token HTTP headers require a trusted tunnel or TLS; plaintext HTTP exposes them on the network. -- kuberay-token (REASONED; kuberay-auth, kuberay-auth-source, kuberay-auth-prior): KubeRay v1.6.0 defines authOptions; v1.5.0 RayClusterSpec lacks it. With token mode enabled, the controller creates a random-token Secret when Kubernetes ServiceAccount token authentication is off, SecretName is null or empty, and the generated-name Secret does not already exist; it leaves an existing generated-name Secret or a supplied named Secret in place and creates no Secret in ServiceAccount mode. The pod builder sets RAY_AUTH_MODE when unset on head and worker Ray containers, on the wait-gcs-ready init container when present, and on the head autoscaler when autoscaling is enabled; outside ServiceAccount mode it adds RAY_AUTH_TOKEN from the selected Secret when unset, and in ServiceAccount mode it does not add RAY_AUTH_TOKEN. This guide infers support in v1.6.0 and later from the compared tags; later releases have not been checked. -- mfa (REASONED; docs): Ray has no user accounts; MFA belongs on SSH, tailnet or identity-aware proxy access. -- grpc-tls (REASONED; docs): RAY_USE_TLS defaults 0; export it and server certificate/key/CA variables on every node before startup for internal gRPC mutual TLS. -- tls-scope (REASONED; docs): gRPC TLS can cost performance and does not replace isolation or the dashboard/Jobs HTTP TLS proxy. -- runtime-code (REASONED; docs): Job runtime_env installs before entrypoint; ray.init environments apply to child tasks/actors. Packages, archives, py_executable and setup hooks execute submitted code. -- runtime-policy (REASONED; docs): No built-in package/URI/hook admission policy; eager_install and setup_timeout_seconds control timing, not trust. Prebuild reviewed dependencies. -- serve-bind (REASONED; docs): Serve HTTP defaults port 8000, Python host 127.0.0.1 but config-file host 0.0.0.0; EveryNode puts proxies on nodes with replicas. -- serve-auth (REASONED; docs): Serve apps have no built-in application auth and are outside cluster token mode; restrict origins and add proxy or FastAPI authentication. -- serve-tls (REASONED; docs): Serve ssl_keyfile/ssl_certfile/ssl_ca_certs default None; supplying a CA does not itself require client certificates. -- serve-grpc (REASONED; docs, ray): Optional Serve gRPC uses 9000 only with configured servicer functions. -- image-privilege (REASONED; ray): Official image ray UID 1000/GID 100 has passwordless sudo; non-root startup alone does not prevent root escalation. -- pod-security (REASONED; kuberay, kubernetes): KubeRay v1.7.0 head/worker pod/container contexts default empty; enforce non-root UID/GID, no privilege escalation, dropped capabilities and RuntimeDefault seccomp. -- verify-inventory (REASONED; ray): Inventory every head/worker and actual randomized agent ports after restarts; expected missing listeners need explanation. No isolated multi-node cluster was available. -- verify-external (REASONED; docs, curl): Probe every routed IPv4/IPv6 origin and inventoried port; completed TCP, even HTTP 401 or non-HTTP, proves exposure. Resolver/local errors are inconclusive. -- verify-jobs (REASONED; ray, kuberay-auth): Through the trusted tunnel valid-token submission must succeed and token-free submission must fail authentication with 401, not a connection error. -- verify-grpc-tls (REASONED; docs): ray health-check must pass with the correct CA and fail certificate verification with a wrong CA/name; local config errors are inconclusive. -- verify-agent (REASONED; ray): Direct non-browser /logs/ must expose an index with token mode off, allow a valid token with it on and reject absent tokens with 401; health exemptions cannot discriminate. -- verify-token-secret (REASONED; ray, curl): Trusted-path hidden prompt and stdin header avoid new token argv/history exposure and set +x avoids tracing; account owner/root can still read memory. - - -Ray's own security page is blunt: if you expose the Ray Dashboard, Ray Jobs, or Ray Client services, "anybody who can access the associated ports can execute arbitrary code on your Ray Cluster", explicitly by submitting a Job or connecting a Client, indirectly through the Dashboard REST API, and implicitly because Ray deserializes arbitrary Python objects with cloudpickle. Ray "doesn't implement access controls for developers interacting with a given cluster"; security and isolation "must be enforced outside of the Ray Cluster". The head ports in question are the dashboard (and Jobs API) on `8265`, the Ray Client server on `10001`, and the head node port `6379`, all plain HTTP or gRPC with no login of their own unless you enable token authentication (step 3). The per-node agent listeners in step 2 add independent paths to job submission, logs, profiling, runtime environments and metrics. - -## 1. Keep the dashboard on loopback - -`ray start --dashboard-host` defaults to localhost (as of Ray 2.58.0, the first address `localhost` resolves to, IPv4 before IPv6, else `127.0.0.1`), and `--dashboard-port` defaults to `8265`. Leave both alone and say so explicitly, because most tutorials and container entrypoints override the host to make the UI reachable: - -```bash -ray start --head --dashboard-host 127.0.0.1 --dashboard-port 8265 -``` +```bash +ray start --head --dashboard-host 127.0.0.1 --dashboard-port 8265 +``` Never pass `--dashboard-host 0.0.0.0` (or `::`) on a machine with a public interface. In Docker the two binds are different things: the host publishes on loopback only (`-p 127.0.0.1:8265:8265`, which Docker's port-publishing docs say only the Docker host can reach, assuming a normal bridge-network NAT setup and a Docker version at or after 28.0.0; the [Docker 28.0.0 release notes](https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/release-notes/28.md#L768-L768) record a fix for neighbour hosts reaching loopback-mapped ports, so this guide assesses earlier releases as potentially allowing hosts on the same layer-2 network to reach a loopback-published port), while inside the container the dashboard must listen on the container's own interface (`--dashboard-host 0.0.0.0` there, private to the Compose network and the host), because a dashboard bound to the container's `127.0.0.1` is not reachable through the published port at all. See [docker.md](docker.md). @@ -598,3 +299,328 @@ REASONED: mixed historical listener observations and unrun service comparisons f - Kubernetes security contexts (runAsNonRoot, drop capabilities, seccomp): https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ - KubeRay ray-cluster Helm chart values (head/worker podSecurityContext and securityContext, default {}): https://github.com/ray-project/kuberay/blob/v1.7.0/helm-chart/ray-cluster/values.yaml - [Ray ray-2.58.0 Serve gRPC default port and servicer-function requirement (checked October 2026)](https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/serve/config.py#L955-L963). + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/ray.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- ray: Ray pinned source (ray-2.58.0) +- token-min: Ray token authentication introduction (2.52.0) +- docs: Ray documentation (unknown) +- redis-history: Ray Redis history announcement (unknown) +- kuberay-auth: KubeRay authentication documentation (unknown) +- kuberay: KubeRay chart (v1.7.0) +- kuberay-auth-source: KubeRay authentication source (v1.6.0) +- kuberay-auth-prior: KubeRay prior RayCluster types (v1.5.0) +- docker: Docker publishing documentation (unknown) +- kubernetes: Kubernetes security contexts (unknown) +- docker-release: Docker Engine release notes (4e9a5751518ed8223a8dcde53693badddd72604f) +- python: CPython WSGI server (v3.11.0) +- curl: curl minimum write-out version (7.75.0) + +Claims (full source mapping in the JSON block below): + +- boundary (REASONED; docs): Dashboard, Jobs and Client access permits arbitrary code execution; use an isolated cluster network and separate clusters for mutually untrusted jobs. +- dashboard (REASONED; ray): Dashboard defaults to resolved localhost, IPv4 before IPv6 then 127.0.0.1, on port 8265; state loopback explicitly. +- docker (REASONED; docker, docker-release): Bridge publishing uses host loopback and container-interface bind; this guide assesses Docker 28.0.0 or later with normal NAT as the loopback-isolation minimum because the 28.0.0 release notes record a fix for neighbour hosts reaching loopback-mapped ports. +- access (REASONED; docs, kuberay-auth): Reach Dashboard/Jobs through loopback SSH forwarding, launcher forwarding, KubeRay forwarding or a private tailnet; public browser access needs an authenticating TLS proxy. +- head-ports (REASONED; docs): Head GCS uses 6379 and Client 10001; worker ports default 10002-19999 plus randomized listeners. +- core-bind (REASONED; ray): Core gRPC source binds all interfaces unless node address is exactly 127.0.0.1, ::1 or localhost; private node IP does not imply private bind. +- gcs-observation (DEMONSTRATED; ray): Four attempted Ray 2.58.0 loopback starts still showed GCS *:6379; the cause was not established and the watcher stopped the runs. +- client-bind (REASONED; ray): Ray Client server/proxier listen on the supplied node address plus loopback; restrict access and prefer Jobs over publishing Client. +- serve-replica (REASONED; ray): Serve inter-deployment gRPC binds [::] on an OS-chosen port; this is source reasoning, not a run. +- agents (REASONED; ray): --include-dashboard=false does not stop dashboard/runtime-env agents; head dashboard itself is reduced to usage stats. +- minimal (REASONED; ray): Missing optional imports select minimal mode: no agent HTTP/gRPC or metrics reporter; runtime-env HTTP remains. Base installs may have full dependencies. +- agent-http (REASONED; ray): Non-minimal agent HTTP defaults 52365, including ray.init; --dashboard-agent-listen-port changes it, with node-IP plus conditional localhost binds. +- agent-jobs (REASONED; ray): Direct /api/job_agent/jobs/ submission and stop/delete/log routes bypass a proxy on 8265; head forwarding reaches its agent job manager. +- agent-placement (REASONED; ray): Worker agents accept cluster submissions with their own managers; drivers normally use the head unless resources, labels or worker-placement override permit workers. +- agent-logs (REASONED; ray): Agent static /logs exposes the node log directory and index. +- agent-http-auth (REASONED; ray): Without token mode a browser heuristic is the only filter; token mode guards all agent HTTP paths except two health endpoints. +- agent-grpc (REASONED; ray): Agent gRPC defaults OS-assigned with --dashboard-agent-grpc-port override; localhost or wildcard bind serves log/profiling RPCs with optional token interception. +- runtime-agent (REASONED; ray): Runtime-env HTTP defaults OS-assigned on node IP with --runtime-env-agent-port override; create/delete installs packages, and token middleware exempts no route. +- metrics-agent (REASONED; ray): Metrics default enabled; non-minimal reporter uses an OS-assigned port or --metrics-export-port, outside all token middleware. +- metrics-ipv4 (REASONED; ray, python): WSGI metrics uses AF_INET: empty address is IPv4 wildcard, localhost is used for local node addresses, and resolved ::1 cannot bind this socket. +- node-normalization (REASONED; ray): Both CLI and ray.init normalize localhost node arguments to a discovered address; cluster-mode setting changes whole-node selection, not independent agent binds. +- agent-family (REASONED; ray): Agent gRPC wildcard selection is 0.0.0.0 or :: based on resolved localhost, independently of node address family. +- gcs-not-redis (REASONED; redis-history, docs): 6379 is GCS, not Redis; Redis stopped being launched by default in 1.11. +- redis-backend (REASONED; docs): External Redis is opt-in GCS fault-tolerance storage; Redis credentials do not authenticate GCS clients and RAY_REDIS_PASSWORD is not a head flag substitute. +- token-default (REASONED; token-min, ray): Shared token authentication exists since 2.52.0 but is disabled by default as of 2.58.0; it supplements network isolation and does not protect Serve apps. +- token-input (REASONED; ray): Enable RAY_AUTH_MODE=token; precedence is RAY_AUTH_TOKEN, RAY_AUTH_TOKEN_PATH then ~/.ray/auth_token, shared by every node/client. +- token-storage (REASONED; ray): Generate/protect token files and copy before startup; tokens are plaintext, do not expire and must never be committed. +- token-transport (REASONED; docs, ray): Token HTTP headers require a trusted tunnel or TLS; plaintext HTTP exposes them on the network. +- kuberay-token (REASONED; kuberay-auth, kuberay-auth-source, kuberay-auth-prior): KubeRay v1.6.0 defines authOptions; v1.5.0 RayClusterSpec lacks it. With token mode enabled, the controller creates a random-token Secret when Kubernetes ServiceAccount token authentication is off, SecretName is null or empty, and the generated-name Secret does not already exist; it leaves an existing generated-name Secret or a supplied named Secret in place and creates no Secret in ServiceAccount mode. The pod builder sets RAY_AUTH_MODE when unset on head and worker Ray containers, on the wait-gcs-ready init container when present, and on the head autoscaler when autoscaling is enabled; outside ServiceAccount mode it adds RAY_AUTH_TOKEN from the selected Secret when unset, and in ServiceAccount mode it does not add RAY_AUTH_TOKEN. This guide infers support in v1.6.0 and later from the compared tags; later releases have not been checked. +- mfa (REASONED; docs): Ray has no user accounts; MFA belongs on SSH, tailnet or identity-aware proxy access. +- grpc-tls (REASONED; docs): RAY_USE_TLS defaults 0; export it and server certificate/key/CA variables on every node before startup for internal gRPC mutual TLS. +- tls-scope (REASONED; docs): gRPC TLS can cost performance and does not replace isolation or the dashboard/Jobs HTTP TLS proxy. +- runtime-code (REASONED; docs): Job runtime_env installs before entrypoint; ray.init environments apply to child tasks/actors. Packages, archives, py_executable and setup hooks execute submitted code. +- runtime-policy (REASONED; docs): No built-in package/URI/hook admission policy; eager_install and setup_timeout_seconds control timing, not trust. Prebuild reviewed dependencies. +- serve-bind (REASONED; docs): Serve HTTP defaults port 8000, Python host 127.0.0.1 but config-file host 0.0.0.0; EveryNode puts proxies on nodes with replicas. +- serve-auth (REASONED; docs): Serve apps have no built-in application auth and are outside cluster token mode; restrict origins and add proxy or FastAPI authentication. +- serve-tls (REASONED; docs): Serve ssl_keyfile/ssl_certfile/ssl_ca_certs default None; supplying a CA does not itself require client certificates. +- serve-grpc (REASONED; docs, ray): Optional Serve gRPC uses 9000 only with configured servicer functions. +- image-privilege (REASONED; ray): Official image ray UID 1000/GID 100 has passwordless sudo; non-root startup alone does not prevent root escalation. +- pod-security (REASONED; kuberay, kubernetes): KubeRay v1.7.0 head/worker pod/container contexts default empty; enforce non-root UID/GID, no privilege escalation, dropped capabilities and RuntimeDefault seccomp. +- verify-inventory (REASONED; ray): Inventory every head/worker and actual randomized agent ports after restarts; expected missing listeners need explanation. No isolated multi-node cluster was available. +- verify-external (REASONED; docs, curl): Probe every routed IPv4/IPv6 origin and inventoried port; completed TCP, even HTTP 401 or non-HTTP, proves exposure. Resolver/local errors are inconclusive. +- verify-jobs (REASONED; ray, kuberay-auth): Through the trusted tunnel valid-token submission must succeed and token-free submission must fail authentication with 401, not a connection error. +- verify-grpc-tls (REASONED; docs): ray health-check must pass with the correct CA and fail certificate verification with a wrong CA/name; local config errors are inconclusive. +- verify-agent (REASONED; ray): Direct non-browser /logs/ must expose an index with token mode off, allow a valid token with it on and reject absent tokens with 401; health exemptions cannot discriminate. +- verify-token-secret (REASONED; ray, curl): Trusted-path hidden prompt and stdin header avoid new token argv/history exposure and set +x avoids tracing; account owner/root can still read memory. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "286cb3508e87ad0658bd57c73270ab3b4100681250bae339f4d71a573e537bfb", + "components": { + "ray": { + "name": "Ray pinned source", + "basis": "ray-2.58.0", + "sources": { + "sed3f197cc1ef": "https://github.com/ray-project/ray/blob/ray-2.58.0/doc/source/ray-security/token-auth.md", + "s08d31327265a": "https://raw.githubusercontent.com/ray-project/ray/ray-2.58.0/docker/base-deps/Dockerfile", + "s2740a1880e2b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/rpc/grpc_server.cc#L67-L68", + "s958054ab801b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.h#L111-L113", + "sa3d2966f536b": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L601-L617", + "s050b349f5bbd": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L185", + "sd7d2d0d963d0": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.cc#L257-L278", + "s329ab4e0ed63": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/serve/_private/replica.py#L1778", + "s3eb2c2cb2cbb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/util/client/server/server.py#L799-L801", + "sae1bb5af6e35": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/util/client/server/proxier.py#L929-L931", + "s207ba00c4e86": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1572-L1575", + "s3efda42d7913": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L2464-L2471", + "s9a060fe08a53": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L189", + "sb1935b32e677": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L618-L635", + "se9af8ec90932": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L687-L692", + "s17bc66b5543e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L20-L23", + "s7df4fce455ca": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L54-L66", + "sc96f32b8a459": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L107-L113", + "sbca5ef4de0bb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/authentication/http_token_authentication.py#L28-L80", + "scdfff3ba79f6": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/optional_utils.py#L132-L209", + "s528944bdec23": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_agent.py#L32-L196", + "s4796e2ca38da": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_head.py#L123-L144", + "s1d86b2f31870": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_head.py#L267-L280", + "s9976ecf5b1f1": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L245-L249", + "sa542601368d1": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L264-L307", + "sd12bacfbc3c7": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L345-L414", + "s7ef1edbcda5c": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L570-L638", + "s6f5310eb313d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L144-L169", + "s8f508c7e2c94": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L182-L187", + "s48b3469099a3": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L100-L117", + "s532a98227d6d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L372-L377", + "s33a5ff7c987b": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1660-L1669", + "s1a19fd793b8f": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1768", + "s8f1ebe4bd911": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1381-L1387", + "s7682a13e83a2": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1862-L1883", + "s9237e74ea054": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1918-L1929", + "s4ce6fe077ba9": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L2022-L2031", + "s7f27125f73b7": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L504-L516", + "sf921a5d6a898": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/prometheus_exporter.py#L326-L334", + "s434c7b4364dc": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L218-L224", + "s08aa128e5d70": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L243-L248", + "sdd8960062a9c": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/includes/network_util.pxi#L103-L110", + "s51476b743c19": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.cc#L280-L289", + "s7451c8e00be3": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L850-L854", + "s6c7faadf88a4": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L786-L821", + "s3625ec280c5e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/worker.py#L1835-L1836", + "sd3a18ab2a112": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L507-L514", + "sba3f67581471": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/parameter.py#L165-L167", + "s994d0ddb6a7d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/worker.py#L1884-L1912", + "sd4d44da36278": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/utils.py#L1128-L1142", + "s5a9d9c171f49": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1909-L1913", + "s9cd95d66d32a": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/utils.py#L322-L351", + "s762672f680e9": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L2099-L2101", + "s28e3a1076d01": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L23-L33", + "s883e3e00e4df": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/common/ray_config_def.h#L618-L619", + "sdb1c4564137b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/raylet/node_manager.cc#L3527-L3530", + "sa7d4f677aadb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_agent.py#L198-L203", + "sa5873642c8a2": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_manager.py#L421-L463", + "s2d562f0e7567": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_manager.py#L565-L609", + "s3a079f7e3d82": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/prometheus_exporter.py#L10", + "sefae1a66c0dd": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L102-L114", + "scc7af6be456e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/serve/config.py#L955-L963" + } + }, + "token-min": { + "name": "Ray token authentication introduction", + "basis": "2.52.0", + "sources": { + "sa5e2c2d58c12": "https://docs.ray.io/en/latest/ray-security/index.html" + } + }, + "docs": { + "name": "Ray documentation", + "basis": "unknown", + "sources": { + "sa5e2c2d58c12": "https://docs.ray.io/en/latest/ray-security/index.html", + "s6f0aa11f549a": "https://docs.ray.io/en/latest/ray-security/token-auth.html", + "s56c7c98ab722": "https://docs.ray.io/en/latest/cluster/cli.html", + "s261570b2672b": "https://docs.ray.io/en/latest/ray-core/configure.html", + "sc375ff2ca01f": "https://docs.ray.io/en/latest/ray-core/handling-dependencies.html", + "sb7a1d0b7dfb3": "https://docs.ray.io/en/latest/ray-core/api/doc/ray.runtime_env.RuntimeEnv.html", + "scd3f7d8a9b02": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.config.HTTPOptions.html", + "s15aa6ec80fb8": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.schema.HTTPOptionsSchema.html", + "s045d75fbdecc": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.schema.ServeDeploySchema.html", + "sc98a37c2b157": "https://docs.ray.io/en/latest/serve/http-guide.html", + "s54ed4b890b65": "https://docs.ray.io/en/latest/cluster/kubernetes/user-guides/kuberay-gcs-ft.html" + } + }, + "redis-history": { + "name": "Ray Redis history announcement", + "basis": "unknown", + "sources": { + "sb5afda59324d": "https://www.anyscale.com/blog/redis-in-ray-past-and-future" + } + }, + "kuberay-auth": { + "name": "KubeRay authentication documentation", + "basis": "unknown", + "sources": { + "saf5247262bc7": "https://docs.ray.io/en/latest/cluster/kubernetes/user-guides/kuberay-auth.html" + } + }, + "kuberay": { + "name": "KubeRay chart", + "basis": "v1.7.0", + "sources": { + "s76e45858aa01": "https://github.com/ray-project/kuberay/blob/v1.7.0/helm-chart/ray-cluster/values.yaml" + } + }, + "kuberay-auth-source": { + "name": "KubeRay authentication source", + "basis": "v1.6.0", + "sources": { + "s9a9f9edcc07e": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/apis/ray/v1/raycluster_types.go#L17-L19", + "s51c84c0bc3b4": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/apis/ray/v1/raycluster_types.go#L81-L107", + "s61e750cdc2e0": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/raycluster_controller.go#L369-L433", + "s6171707b3edb": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L194-L220", + "s408a3a202322": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L234-L236", + "s8505add03e5a": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L253-L271", + "s1cb4f972131a": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L296-L336", + "sd1b672bc4edd": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L460-L462", + "se37635162dc9": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/utils/util.go#L760-L766" + } + }, + "kuberay-auth-prior": { + "name": "KubeRay prior RayCluster types", + "basis": "v1.5.0", + "sources": { + "s04c5ce0b599d": "https://github.com/ray-project/kuberay/blob/v1.5.0/ray-operator/apis/ray/v1/raycluster_types.go#L12-L47" + } + }, + "docker": { + "name": "Docker publishing documentation", + "basis": "unknown", + "sources": { + "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" + } + }, + "kubernetes": { + "name": "Kubernetes security contexts", + "basis": "unknown", + "sources": { + "sb78ea91d0308": "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/" + } + }, + "docker-release": { + "name": "Docker Engine release notes", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "sd733f57ded23": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/release-notes/28.md#L768-L768" + } + }, + "python": { + "name": "CPython WSGI server", + "basis": "v3.11.0", + "sources": { + "sdd5a8e2ab59c": "https://github.com/python/cpython/blob/v3.11.0/Lib/wsgiref/simple_server.py#L1-L160", + "se4933ce3da65": "https://github.com/python/cpython/blob/v3.11.0/Lib/http/server.py#L120-L145", + "s0020052d7267": "https://github.com/python/cpython/blob/v3.11.0/Lib/socketserver.py#L400-L480" + } + }, + "curl": { + "name": "curl minimum write-out version", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + } + }, + "claims": { + "boundary": {"text": "Dashboard, Jobs and Client access permits arbitrary code execution; use an isolated cluster network and separate clusters for mutually untrusted jobs.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12"], "status": "REASONED"}, + "dashboard": {"text": "Dashboard defaults to resolved localhost, IPv4 before IPv6 then 127.0.0.1, on port 8265; state loopback explicitly.", "components": ["ray"], "sources": ["ray:sa3d2966f536b", "ray:s050b349f5bbd", "ray:sd7d2d0d963d0"], "status": "REASONED"}, + "docker": {"text": "Bridge publishing uses host loopback and container-interface bind; this guide assesses Docker 28.0.0 or later with normal NAT as the loopback-isolation minimum because the 28.0.0 release notes record a fix for neighbour hosts reaching loopback-mapped ports.", "components": ["docker", "docker-release"], "sources": ["docker:s1e53417c513d", "docker-release:sd733f57ded23"], "status": "REASONED"}, + "access": {"text": "Reach Dashboard/Jobs through loopback SSH forwarding, launcher forwarding, KubeRay forwarding or a private tailnet; public browser access needs an authenticating TLS proxy.", "components": ["docs", "kuberay-auth"], "sources": ["docs:sa5e2c2d58c12", "docs:s56c7c98ab722", "kuberay-auth:saf5247262bc7"], "status": "REASONED"}, + "head-ports": {"text": "Head GCS uses 6379 and Client 10001; worker ports default 10002-19999 plus randomized listeners.", "components": ["docs"], "sources": ["docs:s56c7c98ab722", "docs:s261570b2672b"], "status": "REASONED"}, + "core-bind": {"text": "Core gRPC source binds all interfaces unless node address is exactly 127.0.0.1, ::1 or localhost; private node IP does not imply private bind.", "components": ["ray"], "sources": ["ray:s2740a1880e2b", "ray:s958054ab801b"], "status": "REASONED"}, + "gcs-observation": {"text": "Four attempted Ray 2.58.0 loopback starts still showed GCS *:6379; the cause was not established and the watcher stopped the runs.", "components": ["ray"], "sources": ["ray:s2740a1880e2b", "ray:s958054ab801b"], "status": "DEMONSTRATED", "evidence": "in each of four runs using `ray start --head --node-ip-address=127.0.0.1 --port=6379 --dashboard-host=127.0.0.1`, `ss` still showed the GCS listener as `*:6379`."}, + "client-bind": {"text": "Ray Client server/proxier listen on the supplied node address plus loopback; restrict access and prefer Jobs over publishing Client.", "components": ["ray"], "sources": ["ray:s3eb2c2cb2cbb", "ray:sae1bb5af6e35", "ray:s207ba00c4e86", "ray:s3efda42d7913"], "status": "REASONED"}, + "serve-replica": {"text": "Serve inter-deployment gRPC binds [::] on an OS-chosen port; this is source reasoning, not a run.", "components": ["ray"], "sources": ["ray:s329ab4e0ed63"], "status": "REASONED"}, + "agents": {"text": "--include-dashboard=false does not stop dashboard/runtime-env agents; head dashboard itself is reduced to usage stats.", "components": ["ray"], "sources": ["ray:s33a5ff7c987b", "ray:s1a19fd793b8f", "ray:s8f1ebe4bd911", "ray:s7682a13e83a2", "ray:s9237e74ea054", "ray:s4ce6fe077ba9"], "status": "REASONED"}, + "minimal": {"text": "Missing optional imports select minimal mode: no agent HTTP/gRPC or metrics reporter; runtime-env HTTP remains. Base installs may have full dependencies.", "components": ["ray"], "sources": ["ray:s48b3469099a3", "ray:sd4d44da36278", "ray:s5a9d9c171f49", "ray:s9cd95d66d32a", "ray:s762672f680e9", "ray:s28e3a1076d01"], "status": "REASONED"}, + "agent-http": {"text": "Non-minimal agent HTTP defaults 52365, including ray.init; --dashboard-agent-listen-port changes it, with node-IP plus conditional localhost binds.", "components": ["ray"], "sources": ["ray:s9a060fe08a53", "ray:sb1935b32e677", "ray:s7df4fce455ca", "ray:sba3f67581471", "ray:s994d0ddb6a7d"], "status": "REASONED"}, + "agent-jobs": {"text": "Direct /api/job_agent/jobs/ submission and stop/delete/log routes bypass a proxy on 8265; head forwarding reaches its agent job manager.", "components": ["ray"], "sources": ["ray:s528944bdec23", "ray:s4796e2ca38da", "ray:s1d86b2f31870"], "status": "REASONED"}, + "agent-placement": {"text": "Worker agents accept cluster submissions with their own managers; drivers normally use the head unless resources, labels or worker-placement override permit workers.", "components": ["ray"], "sources": ["ray:sa7d4f677aadb", "ray:sa5873642c8a2", "ray:s2d562f0e7567"], "status": "REASONED"}, + "agent-logs": {"text": "Agent static /logs exposes the node log directory and index.", "components": ["ray"], "sources": ["ray:s9976ecf5b1f1"], "status": "REASONED"}, + "agent-http-auth": {"text": "Without token mode a browser heuristic is the only filter; token mode guards all agent HTTP paths except two health endpoints.", "components": ["ray"], "sources": ["ray:s17bc66b5543e", "ray:sc96f32b8a459", "ray:sbca5ef4de0bb", "ray:scdfff3ba79f6"], "status": "REASONED"}, + "agent-grpc": {"text": "Agent gRPC defaults OS-assigned with --dashboard-agent-grpc-port override; localhost or wildcard bind serves log/profiling RPCs with optional token interception.", "components": ["ray"], "sources": ["ray:sb1935b32e677", "ray:sa542601368d1", "ray:sd12bacfbc3c7", "ray:s7ef1edbcda5c", "ray:s6f5310eb313d", "ray:s8f508c7e2c94", "ray:s532a98227d6d"], "status": "REASONED"}, + "runtime-agent": {"text": "Runtime-env HTTP defaults OS-assigned on node IP with --runtime-env-agent-port override; create/delete installs packages, and token middleware exempts no route.", "components": ["ray"], "sources": ["ray:s532a98227d6d", "ray:s434c7b4364dc", "ray:s08aa128e5d70", "ray:sb1935b32e677"], "status": "REASONED"}, + "metrics-agent": {"text": "Metrics default enabled; non-minimal reporter uses an OS-assigned port or --metrics-export-port, outside all token middleware.", "components": ["ray"], "sources": ["ray:s532a98227d6d", "ray:s7f27125f73b7", "ray:sf921a5d6a898", "ray:s883e3e00e4df", "ray:sdb1c4564137b", "ray:se9af8ec90932"], "status": "REASONED"}, + "metrics-ipv4": {"text": "WSGI metrics uses AF_INET: empty address is IPv4 wildcard, localhost is used for local node addresses, and resolved ::1 cannot bind this socket.", "components": ["ray", "python"], "sources": ["ray:s7f27125f73b7", "ray:sf921a5d6a898", "ray:s3a079f7e3d82", "python:sdd5a8e2ab59c", "python:se4933ce3da65", "python:s0020052d7267"], "status": "REASONED"}, + "node-normalization": {"text": "Both CLI and ray.init normalize localhost node arguments to a discovered address; cluster-mode setting changes whole-node selection, not independent agent binds.", "components": ["ray"], "sources": ["ray:s7451c8e00be3", "ray:s6c7faadf88a4", "ray:s3625ec280c5e", "ray:sd3a18ab2a112"], "status": "REASONED"}, + "agent-family": {"text": "Agent gRPC wildcard selection is 0.0.0.0 or :: based on resolved localhost, independently of node address family.", "components": ["ray"], "sources": ["ray:sdd8960062a9c", "ray:s51476b743c19"], "status": "REASONED"}, + "gcs-not-redis": {"text": "6379 is GCS, not Redis; Redis stopped being launched by default in 1.11.", "components": ["redis-history", "docs"], "sources": ["redis-history:sb5afda59324d", "docs:s56c7c98ab722"], "status": "REASONED"}, + "redis-backend": {"text": "External Redis is opt-in GCS fault-tolerance storage; Redis credentials do not authenticate GCS clients and RAY_REDIS_PASSWORD is not a head flag substitute.", "components": ["docs"], "sources": ["docs:s56c7c98ab722", "docs:s54ed4b890b65"], "status": "REASONED"}, + "token-default": {"text": "Shared token authentication exists since 2.52.0 but is disabled by default as of 2.58.0; it supplements network isolation and does not protect Serve apps.", "components": ["token-min", "ray"], "sources": ["token-min:sa5e2c2d58c12", "ray:sed3f197cc1ef"], "status": "REASONED"}, + "token-input": {"text": "Enable RAY_AUTH_MODE=token; precedence is RAY_AUTH_TOKEN, RAY_AUTH_TOKEN_PATH then ~/.ray/auth_token, shared by every node/client.", "components": ["ray"], "sources": ["ray:sed3f197cc1ef"], "status": "REASONED"}, + "token-storage": {"text": "Generate/protect token files and copy before startup; tokens are plaintext, do not expire and must never be committed.", "components": ["ray"], "sources": ["ray:sed3f197cc1ef"], "status": "REASONED"}, + "token-transport": {"text": "Token HTTP headers require a trusted tunnel or TLS; plaintext HTTP exposes them on the network.", "components": ["docs", "ray"], "sources": ["docs:s6f0aa11f549a", "ray:sed3f197cc1ef"], "status": "REASONED"}, + "kuberay-token": {"text": "KubeRay v1.6.0 defines authOptions; v1.5.0 RayClusterSpec lacks it. With token mode enabled, the controller creates a random-token Secret when Kubernetes ServiceAccount token authentication is off, SecretName is null or empty, and the generated-name Secret does not already exist; it leaves an existing generated-name Secret or a supplied named Secret in place and creates no Secret in ServiceAccount mode. The pod builder sets RAY_AUTH_MODE when unset on head and worker Ray containers, on the wait-gcs-ready init container when present, and on the head autoscaler when autoscaling is enabled; outside ServiceAccount mode it adds RAY_AUTH_TOKEN from the selected Secret when unset, and in ServiceAccount mode it does not add RAY_AUTH_TOKEN. This guide infers support in v1.6.0 and later from the compared tags; later releases have not been checked.", "components": ["kuberay-auth", "kuberay-auth-source", "kuberay-auth-prior"], "sources": ["kuberay-auth:saf5247262bc7", "kuberay-auth-source:s9a9f9edcc07e", "kuberay-auth-source:s51c84c0bc3b4", "kuberay-auth-source:s61e750cdc2e0", "kuberay-auth-source:s6171707b3edb", "kuberay-auth-source:s408a3a202322", "kuberay-auth-source:s8505add03e5a", "kuberay-auth-source:s1cb4f972131a", "kuberay-auth-source:sd1b672bc4edd", "kuberay-auth-source:se37635162dc9", "kuberay-auth-prior:s04c5ce0b599d"], "status": "REASONED"}, + "mfa": {"text": "Ray has no user accounts; MFA belongs on SSH, tailnet or identity-aware proxy access.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12"], "status": "REASONED"}, + "grpc-tls": {"text": "RAY_USE_TLS defaults 0; export it and server certificate/key/CA variables on every node before startup for internal gRPC mutual TLS.", "components": ["docs"], "sources": ["docs:s261570b2672b"], "status": "REASONED"}, + "tls-scope": {"text": "gRPC TLS can cost performance and does not replace isolation or the dashboard/Jobs HTTP TLS proxy.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12", "docs:s261570b2672b"], "status": "REASONED"}, + "runtime-code": {"text": "Job runtime_env installs before entrypoint; ray.init environments apply to child tasks/actors. Packages, archives, py_executable and setup hooks execute submitted code.", "components": ["docs"], "sources": ["docs:sc375ff2ca01f"], "status": "REASONED"}, + "runtime-policy": {"text": "No built-in package/URI/hook admission policy; eager_install and setup_timeout_seconds control timing, not trust. Prebuild reviewed dependencies.", "components": ["docs"], "sources": ["docs:sc375ff2ca01f", "docs:sb7a1d0b7dfb3"], "status": "REASONED"}, + "serve-bind": {"text": "Serve HTTP defaults port 8000, Python host 127.0.0.1 but config-file host 0.0.0.0; EveryNode puts proxies on nodes with replicas.", "components": ["docs"], "sources": ["docs:scd3f7d8a9b02", "docs:s15aa6ec80fb8", "docs:s045d75fbdecc"], "status": "REASONED"}, + "serve-auth": {"text": "Serve apps have no built-in application auth and are outside cluster token mode; restrict origins and add proxy or FastAPI authentication.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12", "docs:sc98a37c2b157"], "status": "REASONED"}, + "serve-tls": {"text": "Serve ssl_keyfile/ssl_certfile/ssl_ca_certs default None; supplying a CA does not itself require client certificates.", "components": ["docs"], "sources": ["docs:scd3f7d8a9b02"], "status": "REASONED"}, + "serve-grpc": {"text": "Optional Serve gRPC uses 9000 only with configured servicer functions.", "components": ["docs", "ray"], "sources": ["docs:s045d75fbdecc", "ray:scc7af6be456e"], "status": "REASONED"}, + "image-privilege": {"text": "Official image ray UID 1000/GID 100 has passwordless sudo; non-root startup alone does not prevent root escalation.", "components": ["ray"], "sources": ["ray:s08d31327265a"], "status": "REASONED"}, + "pod-security": {"text": "KubeRay v1.7.0 head/worker pod/container contexts default empty; enforce non-root UID/GID, no privilege escalation, dropped capabilities and RuntimeDefault seccomp.", "components": ["kuberay", "kubernetes"], "sources": ["kuberay:s76e45858aa01", "kubernetes:sb78ea91d0308"], "status": "REASONED"}, + "verify-inventory": {"text": "Inventory every head/worker and actual randomized agent ports after restarts; expected missing listeners need explanation. No isolated multi-node cluster was available.", "components": ["ray"], "sources": ["ray:s9a060fe08a53", "ray:s6f5310eb313d", "ray:s532a98227d6d", "ray:s7f27125f73b7", "ray:s434c7b4364dc"], "status": "REASONED", "verify": [1]}, + "verify-external": {"text": "Probe every routed IPv4/IPv6 origin and inventoried port; completed TCP, even HTTP 401 or non-HTTP, proves exposure. Resolver/local errors are inconclusive.", "components": ["docs", "curl"], "sources": ["docs:sa5e2c2d58c12", "docs:s261570b2672b", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, + "verify-jobs": {"text": "Through the trusted tunnel valid-token submission must succeed and token-free submission must fail authentication with 401, not a connection error.", "components": ["ray", "kuberay-auth"], "sources": ["ray:sed3f197cc1ef", "kuberay-auth:saf5247262bc7"], "status": "REASONED", "verify": [1]}, + "verify-grpc-tls": {"text": "ray health-check must pass with the correct CA and fail certificate verification with a wrong CA/name; local config errors are inconclusive.", "components": ["docs"], "sources": ["docs:s261570b2672b"], "status": "REASONED", "verify": [1]}, + "verify-agent": {"text": "Direct non-browser /logs/ must expose an index with token mode off, allow a valid token with it on and reject absent tokens with 401; health exemptions cannot discriminate.", "components": ["ray"], "sources": ["ray:s9976ecf5b1f1", "ray:s17bc66b5543e", "ray:sbca5ef4de0bb", "ray:sefae1a66c0dd"], "status": "REASONED", "verify": [2]}, + "verify-token-secret": {"text": "Trusted-path hidden prompt and stdin header avoid new token argv/history exposure and set +x avoids tracing; account owner/root can still read memory.", "components": ["ray", "curl"], "sources": ["ray:sed3f197cc1ef", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/realtime-webhooks.md b/plugin/skills/secure-deployment-config/references/realtime-webhooks.md index cd87fb2f4..5823d7062 100644 --- a/plugin/skills/secure-deployment-config/references/realtime-webhooks.md +++ b/plugin/skills/secure-deployment-config/references/realtime-webhooks.md @@ -1,6 +1,6 @@ # WebSockets, server-sent events, and webhooks: authenticating the non-page endpoints -Written against WebSocket protocol RFC 6455, stripe-node v18.5.0, stripe-python v12.5.0, Traefik timeout reference v3.5, Caddy source v2.11.4, nginx source release-1.26.3, HAProxy 3.0, and HAProxy source v3.0.0, plus 1 more recorded bases; 7 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 36 claims, all reasoned from cited sources. +Written against WebSocket protocol RFC 6455, stripe-node v18.5.0, stripe-python v12.5.0, Traefik timeout reference v3.5, Caddy source v2.11.4, nginx source release-1.26.3, HAProxy 3.0, and HAProxy source v3.0.0, plus 1 more recorded basis; 7 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 36 claims, all reasoned from cited sources. A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. diff --git a/plugin/skills/secure-deployment-config/references/self-hosted-ci-runners.md b/plugin/skills/secure-deployment-config/references/self-hosted-ci-runners.md index ec98ac060..fc8863802 100644 --- a/plugin/skills/secure-deployment-config/references/self-hosted-ci-runners.md +++ b/plugin/skills/secure-deployment-config/references/self-hosted-ci-runners.md @@ -1,167 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-12", - "documentation_checked": "2026-09", - "body_sha256": "72e6c5e6e8aa98cd95989f98eb383a327daaecd20809fcb2d24aea181dee4f4e", - "components": { - "github": { - "name": "GitHub Actions documentation", - "basis": "unknown", - "sources": { - "s215dc07e3642": "https://docs.github.com/en/actions/reference/security/secure-use", - "s92e8e4820392": "https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/manage-access", - "sbbcb15f795a8": "https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows", - "s4561d20e7042": "https://docs.github.com/en/actions/concepts/security/github_token", - "s80fe11d8c90d": "https://docs.github.com/en/actions/reference/security/oidc" - } - }, - "github-api": { - "name": "GitHub REST API", - "basis": "2026-03-10", - "sources": { - "s8217448953a4": "https://docs.github.com/en/rest/actions/self-hosted-runners", - "sd36db9ea1031": "https://docs.github.com/en/rest/actions/self-hosted-runner-groups", - "sbeee4a47110c": "https://docs.github.com/en/rest/actions/workflow-jobs" - } - }, - "gitlab": { - "name": "GitLab rolling documentation", - "basis": "unknown", - "sources": { - "seb30e3937183": "https://docs.gitlab.com/runner/security/", - "sde661ff4762e": "https://docs.gitlab.com/runner/executors/docker_autoscaler/", - "s265254bc5592": "https://docs.gitlab.com/runner/executors/docker/", - "s47b236d63dc4": "https://docs.gitlab.com/runner/configuration/advanced-configuration/", - "s1579c499c75b": "https://docs.gitlab.com/runner/executors/kubernetes/", - "sd2cbff5d5d6e": "https://docs.gitlab.com/ci/runners/new_creation_workflow/", - "se430cbabc011": "https://docs.gitlab.com/api/users/#create-a-runner-linked-to-a-user", - "saef0483b7be7": "https://docs.gitlab.com/ci/jobs/ci_job_token/", - "s699c1c198119": "https://docs.gitlab.com/api/project_job_token_scopes/", - "sbc009ff2866a": "https://docs.gitlab.com/runner/monitoring/" - } - }, - "python": { - "name": "Python tomllib minimum", - "basis": "3.11", - "sources": { - "s37be9e98ea29": "https://docs.python.org/3.11/library/tomllib.html" - } - }, - "docker": { - "name": "Docker Engine documentation", - "basis": "unknown", - "sources": { - "s18f59818e451": "https://docs.docker.com/engine/security/" - } - }, - "aws": { - "name": "AWS EC2 documentation", - "basis": "unknown", - "sources": { - "s82aca706445c": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html" - } - } - }, - "claims": { - "job-transport": {"text": "Both runners poll over HTTPS without an inbound job-polling port; verify GitLab TLS and restrict separate autoscaler management or webhook listeners.", "components": ["github", "gitlab"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "gitlab:sde661ff4762e"], "status": "REASONED"}, - "worker-account": {"text": "Use a dedicated least-privileged account, keep provisioning credentials off workers and confine rootful Docker access to disposable VMs.", "components": ["github", "gitlab", "docker"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "docker:s18f59818e451"], "status": "REASONED"}, - "worker-egress": {"text": "Enforce job egress outside job control, isolate production and other workers, and deny unused metadata over both families; IMDSv2 does not stop arbitrary authorized job code.", "components": ["github", "gitlab", "aws"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "aws:s82aca706445c"], "status": "REASONED"}, - "github-eligibility": {"text": "Keep self-hosted runners off public repositories; runner groups admit only private repositories by default and the public override should stay off.", "components": ["github"], "sources": ["github:s92e8e4820392", "github:s215dc07e3642"], "status": "REASONED"}, - "fork-token": {"text": "Ordinary fork pull_request jobs normally receive a read-only GITHUB_TOKEN and no other secrets, but can still read host credentials and reachable services.", "components": ["github"], "sources": ["github:s215dc07e3642", "github:sbbcb15f795a8"], "status": "REASONED"}, - "privileged-triggers": {"text": "pull_request_target and workflow_run can access secrets and write tokens; never execute untrusted PR code, artifacts or caches in these jobs.", "components": ["github"], "sources": ["github:sbbcb15f795a8", "github:s215dc07e3642"], "status": "REASONED"}, - "private-forks": {"text": "Review private-repository fork settings, which can grant write tokens and secrets; separate trusted privileged workers from untrusted CI.", "components": ["github"], "sources": ["github:s215dc07e3642"], "status": "REASONED"}, - "registration-token": {"text": "Registration tokens last one hour; generate on demand, never bake into images, and do not confuse expiry with runner deregistration.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, - "registration-manager": {"text": "Keep the App/PAT generating registration or JIT credentials off workers; organization endpoints need Self-hosted runners: write, repository endpoints Administration: write.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, - "jit": {"text": "Protect encoded_jit_config and the whole response; JIT runners execute at most one job.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, - "runner-credential": {"text": "Protect the installed runner authentication credential, directory and backups.", "components": ["github"], "sources": ["github:s215dc07e3642"], "status": "REASONED"}, - "github-job-token": {"text": "GITHUB_TOKEN is repository-scoped and per-job; set read-only defaults and grant only trusted jobs additional permissions.", "components": ["github"], "sources": ["github:s4561d20e7042"], "status": "REASONED"}, - "oidc": {"text": "Grant id-token: write only where needed; the cloud trust must restrict issuer, audience and repository/branch or protected-environment subject.", "components": ["github"], "sources": ["github:s80fe11d8c90d"], "status": "REASONED"}, - "github-lifecycle": {"text": "Prefer ephemeral/JIT workers and destroy their machines and storage; deregistration does not destroy disks, and labels route rather than authorize jobs.", "components": ["github", "github-api"], "sources": ["github:s215dc07e3642", "github:s92e8e4820392", "github-api:s8217448953a4"], "status": "REASONED"}, - "shell-executor": {"text": "The GitLab shell executor runs as the runner user without isolation; avoid it for untrusted jobs on shared hosts.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183"], "status": "REASONED"}, - "docker-executor": {"text": "Use unprivileged Docker; privileged=true or services_privileged and host Docker socket/TCP access can expose the host despite privileged=false.", "components": ["gitlab", "docker"], "sources": ["gitlab:s265254bc5592", "gitlab:seb30e3937183", "docker:s18f59818e451"], "status": "REASONED"}, - "executor-host-access": {"text": "Deny sensitive host mounts, host PID/network namespaces, unnecessary devices and capabilities; build without the host daemon.", "components": ["gitlab"], "sources": ["gitlab:s265254bc5592", "gitlab:s47b236d63dc4"], "status": "REASONED"}, - "kubernetes-executor": {"text": "Enforce nonprivileged containers, no privilege escalation, scoped ServiceAccounts and no unnecessary token mounting; prevent job overrides.", "components": ["gitlab"], "sources": ["gitlab:s1579c499c75b"], "status": "REASONED"}, - "gitlab-disposal": {"text": "Containers share the kernel; use disposable separate VMs/nodes as needed and one-job autoscaling workers. Instance and Docker Autoscaler GA is recorded as Runner 17.1.", "components": ["gitlab"], "sources": ["gitlab:sde661ff4762e", "gitlab:seb30e3937183"], "status": "REASONED"}, - "gitlab-registration": {"text": "Legacy registration tokens were disabled by default in GitLab 17.0, with removal scheduled for 20.0; use runner authentication tokens.", "components": ["gitlab"], "sources": ["gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, - "gitlab-runner-token": {"text": "Protect and rotate the runner authentication token in config.toml and backups; a cloned runner can steal assigned jobs.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, - "gitlab-creation": {"text": "POST /user/runners needs create_runner scope; keep that access token off workers and set scope/protection at creation.", "components": ["gitlab"], "sources": ["gitlab:se430cbabc011", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, - "gitlab-job-token": {"text": "CI_JOB_TOKEN carries supported triggering-user access during the job and appears as CI_REGISTRY_PASSWORD and inside CI_REPOSITORY_URL.", "components": ["gitlab"], "sources": ["gitlab:saef0483b7be7"], "status": "REASONED"}, - "gitlab-allowlist": {"text": "Target-project inbound job-token allowlists limit who may access that target, not job egress; also restrict resource feature visibility.", "components": ["gitlab"], "sources": ["gitlab:saef0483b7be7", "gitlab:s699c1c198119"], "status": "REASONED"}, - "gitlab-forks": {"text": "Instance runners on public installations accept strangers; project scope does not establish code trust. Review parent-launched fork pipelines or disable ci_allow_fork_pipelines_to_run_in_parent_project.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183"], "status": "REASONED"}, - "gitlab-protected": {"text": "Developer access can compromise persistent runners; release runners need ref_protected and a pool separate from fork-merge-request testing.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, - "gitlab-metrics": {"text": "Optional listen_address metrics and profiling have no built-in authorization; 9252 is allocated, not proof of the configured port.", "components": ["gitlab"], "sources": ["gitlab:sbc009ff2866a"], "status": "REASONED"}, - "gitlab-session": {"text": "Optional session_server uses TLS and must admit required GitLab traffic at its configured address; 8093 is an example, not an established default.", "components": ["gitlab"], "sources": ["gitlab:s47b236d63dc4"], "status": "REASONED"}, - "verify-eligibility": {"text": "Fully paginate groups, selected repositories and public repositories' own runners; visibility=all alone is not public eligibility, and inherited access needs review.", "components": ["github-api"], "sources": ["github-api:sd36db9ea1031", "github-api:s8217448953a4"], "status": "REASONED", "verify": [1]}, - "verify-permissions": {"text": "Inspect organization and repository workflow-token defaults and PR approval settings; explicit workflow permissions can raise grants and id-token is separate.", "components": ["github"], "sources": ["github:s4561d20e7042", "github:s80fe11d8c90d"], "status": "REASONED", "verify": [2]}, - "verify-assignment": {"text": "A controlled fork workflow should run when exposed and lose assignment when restricted, while an allowed private job still runs; skipped/pending/offline/timeout is inconclusive.", "components": ["github-api", "github"], "sources": ["github-api:sbeee4a47110c", "github:s92e8e4820392"], "status": "REASONED", "verify": [3]}, - "verify-config": {"text": "Python 3.14.4 synthetic fixtures reported exposed/fixed Docker, Kubernetes and autoscaler values; wrong types passed through, misplaced keys printed null, and empty/error cases differed in output and exit status.", "components": ["python", "gitlab"], "sources": ["python:s37be9e98ea29", "gitlab:s47b236d63dc4", "gitlab:s1579c499c75b"], "status": "DEMONSTRATED", "verify": [4], "evidence": "the extracted Python program ran without sudo on 20 synthetic config.toml cases using Python 3.14.4. Docker fixtures printed true/true, a socket mount and a TCP host versus false/false, empty volumes and a null host; both exited 0. Autoscaler limits printed null/null, 1/2 and 2/1 versus 1/1, all exit 0. Kubernetes fixtures printed true versus false for the three booleans and the supplied service_account. Wrong-type strings, booleans and floats retained their JSON types; misplaced privileged printed docker.privileged null. Empty and runner-less files printed nothing and exited 0. Missing files, malformed TOML and wrong section types exited 1; the second-runner structure error followed one printed runner. Two valid runners printed indices 0 and 1. The runner token key was omitted; a dummy token copied into service_account was printed. These observations cover file inspection only."}, - "verify-daemon": {"text": "Resolve docker.host through DOCKER_HOST or the default socket when absent; flag actual job access to a rootful daemon, not tcp:// alone.", "components": ["gitlab", "docker"], "sources": ["gitlab:s265254bc5592", "docker:s18f59818e451"], "status": "REASONED"}, - "verify-autoscaler": {"text": "For Instance/Docker Autoscaler require capacity_per_instance=1 and max_use_count=1 explicitly as integers; absent or other values are findings.", "components": ["gitlab"], "sources": ["gitlab:sde661ff4762e"], "status": "REASONED"}, - "verify-gitlab-api": {"text": "Read inbound allowlists and require release-runner ref_protected against exposed controls; gitlab-runner verify proves authentication only.", "components": ["gitlab"], "sources": ["gitlab:s699c1c198119", "gitlab:seb30e3937183"], "status": "REASONED", "verify": [5]}, - "verify-lifecycle": {"text": "Check complete deregistration listings and infrastructure disposal; persistent-worker canary must survive in the control, but absence alone does not prove destruction or clean shared caches.", "components": ["github-api", "gitlab", "github"], "sources": ["github-api:s8217448953a4", "gitlab:sde661ff4762e", "github:s215dc07e3642"], "status": "REASONED"}, - "verify-listeners": {"text": "Inventory namespaces and publications, require the permitted-client response, then test disallowed access; any HTTP response proves reachability, failure needs corroborating firewall denial.", "components": ["gitlab"], "sources": ["gitlab:sbc009ff2866a", "gitlab:s47b236d63dc4"], "status": "REASONED", "verify": [6]} - } -} ---- # Self-hosted CI runners: GitHub Actions and GitLab Runner - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-12; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- github: GitHub Actions documentation (unknown) -- github-api: GitHub REST API (2026-03-10) -- gitlab: GitLab rolling documentation (unknown) -- python: Python tomllib minimum (3.11) -- docker: Docker Engine documentation (unknown) -- aws: AWS EC2 documentation (unknown) +Written against GitHub Actions documentation (no recorded version), GitHub REST API 2026-03-10, and Python tomllib minimum 3.11; 3 further sources carry no recorded version. Metadata reviewed 2026-10-12; documentation checked September 2026. The guide records 37 claims: 1 demonstrated by a check that ran and 36 reasoned from cited sources. -Claims (full source mapping in the front matter): - -- job-transport (REASONED; github, gitlab): Both runners poll over HTTPS without an inbound job-polling port; verify GitLab TLS and restrict separate autoscaler management or webhook listeners. -- worker-account (REASONED; github, gitlab, docker): Use a dedicated least-privileged account, keep provisioning credentials off workers and confine rootful Docker access to disposable VMs. -- worker-egress (REASONED; github, gitlab, aws): Enforce job egress outside job control, isolate production and other workers, and deny unused metadata over both families; IMDSv2 does not stop arbitrary authorized job code. -- github-eligibility (REASONED; github): Keep self-hosted runners off public repositories; runner groups admit only private repositories by default and the public override should stay off. -- fork-token (REASONED; github): Ordinary fork pull_request jobs normally receive a read-only GITHUB_TOKEN and no other secrets, but can still read host credentials and reachable services. -- privileged-triggers (REASONED; github): pull_request_target and workflow_run can access secrets and write tokens; never execute untrusted PR code, artifacts or caches in these jobs. -- private-forks (REASONED; github): Review private-repository fork settings, which can grant write tokens and secrets; separate trusted privileged workers from untrusted CI. -- registration-token (REASONED; github-api): Registration tokens last one hour; generate on demand, never bake into images, and do not confuse expiry with runner deregistration. -- registration-manager (REASONED; github-api): Keep the App/PAT generating registration or JIT credentials off workers; organization endpoints need Self-hosted runners: write, repository endpoints Administration: write. -- jit (REASONED; github-api): Protect encoded_jit_config and the whole response; JIT runners execute at most one job. -- runner-credential (REASONED; github): Protect the installed runner authentication credential, directory and backups. -- github-job-token (REASONED; github): GITHUB_TOKEN is repository-scoped and per-job; set read-only defaults and grant only trusted jobs additional permissions. -- oidc (REASONED; github): Grant id-token: write only where needed; the cloud trust must restrict issuer, audience and repository/branch or protected-environment subject. -- github-lifecycle (REASONED; github, github-api): Prefer ephemeral/JIT workers and destroy their machines and storage; deregistration does not destroy disks, and labels route rather than authorize jobs. -- shell-executor (REASONED; gitlab): The GitLab shell executor runs as the runner user without isolation; avoid it for untrusted jobs on shared hosts. -- docker-executor (REASONED; gitlab, docker): Use unprivileged Docker; privileged=true or services_privileged and host Docker socket/TCP access can expose the host despite privileged=false. -- executor-host-access (REASONED; gitlab): Deny sensitive host mounts, host PID/network namespaces, unnecessary devices and capabilities; build without the host daemon. -- kubernetes-executor (REASONED; gitlab): Enforce nonprivileged containers, no privilege escalation, scoped ServiceAccounts and no unnecessary token mounting; prevent job overrides. -- gitlab-disposal (REASONED; gitlab): Containers share the kernel; use disposable separate VMs/nodes as needed and one-job autoscaling workers. Instance and Docker Autoscaler GA is recorded as Runner 17.1. -- gitlab-registration (REASONED; gitlab): Legacy registration tokens were disabled by default in GitLab 17.0, with removal scheduled for 20.0; use runner authentication tokens. -- gitlab-runner-token (REASONED; gitlab): Protect and rotate the runner authentication token in config.toml and backups; a cloned runner can steal assigned jobs. -- gitlab-creation (REASONED; gitlab): POST /user/runners needs create_runner scope; keep that access token off workers and set scope/protection at creation. -- gitlab-job-token (REASONED; gitlab): CI_JOB_TOKEN carries supported triggering-user access during the job and appears as CI_REGISTRY_PASSWORD and inside CI_REPOSITORY_URL. -- gitlab-allowlist (REASONED; gitlab): Target-project inbound job-token allowlists limit who may access that target, not job egress; also restrict resource feature visibility. -- gitlab-forks (REASONED; gitlab): Instance runners on public installations accept strangers; project scope does not establish code trust. Review parent-launched fork pipelines or disable ci_allow_fork_pipelines_to_run_in_parent_project. -- gitlab-protected (REASONED; gitlab): Developer access can compromise persistent runners; release runners need ref_protected and a pool separate from fork-merge-request testing. -- gitlab-metrics (REASONED; gitlab): Optional listen_address metrics and profiling have no built-in authorization; 9252 is allocated, not proof of the configured port. -- gitlab-session (REASONED; gitlab): Optional session_server uses TLS and must admit required GitLab traffic at its configured address; 8093 is an example, not an established default. -- verify-eligibility (REASONED; github-api): Fully paginate groups, selected repositories and public repositories' own runners; visibility=all alone is not public eligibility, and inherited access needs review. -- verify-permissions (REASONED; github): Inspect organization and repository workflow-token defaults and PR approval settings; explicit workflow permissions can raise grants and id-token is separate. -- verify-assignment (REASONED; github-api, github): A controlled fork workflow should run when exposed and lose assignment when restricted, while an allowed private job still runs; skipped/pending/offline/timeout is inconclusive. -- verify-config (DEMONSTRATED; python, gitlab): Python 3.14.4 synthetic fixtures reported exposed/fixed Docker, Kubernetes and autoscaler values; wrong types passed through, misplaced keys printed null, and empty/error cases differed in output and exit status. -- verify-daemon (REASONED; gitlab, docker): Resolve docker.host through DOCKER_HOST or the default socket when absent; flag actual job access to a rootful daemon, not tcp:// alone. -- verify-autoscaler (REASONED; gitlab): For Instance/Docker Autoscaler require capacity_per_instance=1 and max_use_count=1 explicitly as integers; absent or other values are findings. -- verify-gitlab-api (REASONED; gitlab): Read inbound allowlists and require release-runner ref_protected against exposed controls; gitlab-runner verify proves authentication only. -- verify-lifecycle (REASONED; github-api, gitlab, github): Check complete deregistration listings and infrastructure disposal; persistent-worker canary must survive in the control, but absence alone does not prove destruction or clean shared caches. -- verify-listeners (REASONED; gitlab): Inventory namespaces and publications, require the permitted-client response, then test disallowed access; any HTTP response proves reachability, failure needs corroborating firewall denial. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. A self-hosted runner exists to execute CI job commands on your own machine, so the trust question is not a bind address but who can cause code to run on it. Both GitHub Actions runners and GitLab Runner @@ -433,3 +274,188 @@ Version boundary: GitHub.com documentation checked 18 September 2026, with REST - Docker Engine security (rootful daemon, docker group as host administration): https://docs.docker.com/engine/security/ - AWS EC2 instance metadata service (IMDSv2 does not stop authorized in-job requests): https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html - Python tomllib (offline config.toml inspection, Python 3.11+): https://docs.python.org/3.11/library/tomllib.html + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/self-hosted-ci-runners.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- github: GitHub Actions documentation (unknown) +- github-api: GitHub REST API (2026-03-10) +- gitlab: GitLab rolling documentation (unknown) +- docker: Docker Engine documentation (unknown) +- python: Python tomllib minimum (3.11) +- aws: AWS EC2 documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- job-transport (REASONED; github, gitlab): Both runners poll over HTTPS without an inbound job-polling port; verify GitLab TLS and restrict separate autoscaler management or webhook listeners. +- worker-account (REASONED; github, gitlab, docker): Use a dedicated least-privileged account, keep provisioning credentials off workers and confine rootful Docker access to disposable VMs. +- worker-egress (REASONED; github, gitlab, aws): Enforce job egress outside job control, isolate production and other workers, and deny unused metadata over both families; IMDSv2 does not stop arbitrary authorized job code. +- github-eligibility (REASONED; github): Keep self-hosted runners off public repositories; runner groups admit only private repositories by default and the public override should stay off. +- fork-token (REASONED; github): Ordinary fork pull_request jobs normally receive a read-only GITHUB_TOKEN and no other secrets, but can still read host credentials and reachable services. +- privileged-triggers (REASONED; github): pull_request_target and workflow_run can access secrets and write tokens; never execute untrusted PR code, artifacts or caches in these jobs. +- private-forks (REASONED; github): Review private-repository fork settings, which can grant write tokens and secrets; separate trusted privileged workers from untrusted CI. +- registration-token (REASONED; github-api): Registration tokens last one hour; generate on demand, never bake into images, and do not confuse expiry with runner deregistration. +- registration-manager (REASONED; github-api): Keep the App/PAT generating registration or JIT credentials off workers; organization endpoints need Self-hosted runners: write, repository endpoints Administration: write. +- jit (REASONED; github-api): Protect encoded_jit_config and the whole response; JIT runners execute at most one job. +- runner-credential (REASONED; github): Protect the installed runner authentication credential, directory and backups. +- github-job-token (REASONED; github): GITHUB_TOKEN is repository-scoped and per-job; set read-only defaults and grant only trusted jobs additional permissions. +- oidc (REASONED; github): Grant id-token: write only where needed; the cloud trust must restrict issuer, audience and repository/branch or protected-environment subject. +- github-lifecycle (REASONED; github, github-api): Prefer ephemeral/JIT workers and destroy their machines and storage; deregistration does not destroy disks, and labels route rather than authorize jobs. +- shell-executor (REASONED; gitlab): The GitLab shell executor runs as the runner user without isolation; avoid it for untrusted jobs on shared hosts. +- docker-executor (REASONED; gitlab, docker): Use unprivileged Docker; privileged=true or services_privileged and host Docker socket/TCP access can expose the host despite privileged=false. +- executor-host-access (REASONED; gitlab): Deny sensitive host mounts, host PID/network namespaces, unnecessary devices and capabilities; build without the host daemon. +- kubernetes-executor (REASONED; gitlab): Enforce nonprivileged containers, no privilege escalation, scoped ServiceAccounts and no unnecessary token mounting; prevent job overrides. +- gitlab-disposal (REASONED; gitlab): Containers share the kernel; use disposable separate VMs/nodes as needed and one-job autoscaling workers. Instance and Docker Autoscaler GA is recorded as Runner 17.1. +- gitlab-registration (REASONED; gitlab): Legacy registration tokens were disabled by default in GitLab 17.0, with removal scheduled for 20.0; use runner authentication tokens. +- gitlab-runner-token (REASONED; gitlab): Protect and rotate the runner authentication token in config.toml and backups; a cloned runner can steal assigned jobs. +- gitlab-creation (REASONED; gitlab): POST /user/runners needs create_runner scope; keep that access token off workers and set scope/protection at creation. +- gitlab-job-token (REASONED; gitlab): CI_JOB_TOKEN carries supported triggering-user access during the job and appears as CI_REGISTRY_PASSWORD and inside CI_REPOSITORY_URL. +- gitlab-allowlist (REASONED; gitlab): Target-project inbound job-token allowlists limit who may access that target, not job egress; also restrict resource feature visibility. +- gitlab-forks (REASONED; gitlab): Instance runners on public installations accept strangers; project scope does not establish code trust. Review parent-launched fork pipelines or disable ci_allow_fork_pipelines_to_run_in_parent_project. +- gitlab-protected (REASONED; gitlab): Developer access can compromise persistent runners; release runners need ref_protected and a pool separate from fork-merge-request testing. +- gitlab-metrics (REASONED; gitlab): Optional listen_address metrics and profiling have no built-in authorization; 9252 is allocated, not proof of the configured port. +- gitlab-session (REASONED; gitlab): Optional session_server uses TLS and must admit required GitLab traffic at its configured address; 8093 is an example, not an established default. +- verify-eligibility (REASONED; github-api): Fully paginate groups, selected repositories and public repositories' own runners; visibility=all alone is not public eligibility, and inherited access needs review. +- verify-permissions (REASONED; github): Inspect organization and repository workflow-token defaults and PR approval settings; explicit workflow permissions can raise grants and id-token is separate. +- verify-assignment (REASONED; github-api, github): A controlled fork workflow should run when exposed and lose assignment when restricted, while an allowed private job still runs; skipped/pending/offline/timeout is inconclusive. +- verify-config (DEMONSTRATED; python, gitlab): Python 3.14.4 synthetic fixtures reported exposed/fixed Docker, Kubernetes and autoscaler values; wrong types passed through, misplaced keys printed null, and empty/error cases differed in output and exit status. +- verify-daemon (REASONED; gitlab, docker): Resolve docker.host through DOCKER_HOST or the default socket when absent; flag actual job access to a rootful daemon, not tcp:// alone. +- verify-autoscaler (REASONED; gitlab): For Instance/Docker Autoscaler require capacity_per_instance=1 and max_use_count=1 explicitly as integers; absent or other values are findings. +- verify-gitlab-api (REASONED; gitlab): Read inbound allowlists and require release-runner ref_protected against exposed controls; gitlab-runner verify proves authentication only. +- verify-lifecycle (REASONED; github-api, gitlab, github): Check complete deregistration listings and infrastructure disposal; persistent-worker canary must survive in the control, but absence alone does not prove destruction or clean shared caches. +- verify-listeners (REASONED; gitlab): Inventory namespaces and publications, require the permitted-client response, then test disallowed access; any HTTP response proves reachability, failure needs corroborating firewall denial. + +````json +{ + "schema": 1, + "checked": "2026-10-12", + "documentation_checked": "2026-09", + "body_sha256": "72e6c5e6e8aa98cd95989f98eb383a327daaecd20809fcb2d24aea181dee4f4e", + "components": { + "github": { + "name": "GitHub Actions documentation", + "basis": "unknown", + "sources": { + "s215dc07e3642": "https://docs.github.com/en/actions/reference/security/secure-use", + "s92e8e4820392": "https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/manage-access", + "sbbcb15f795a8": "https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows", + "s4561d20e7042": "https://docs.github.com/en/actions/concepts/security/github_token", + "s80fe11d8c90d": "https://docs.github.com/en/actions/reference/security/oidc" + } + }, + "github-api": { + "name": "GitHub REST API", + "basis": "2026-03-10", + "sources": { + "s8217448953a4": "https://docs.github.com/en/rest/actions/self-hosted-runners", + "sd36db9ea1031": "https://docs.github.com/en/rest/actions/self-hosted-runner-groups", + "sbeee4a47110c": "https://docs.github.com/en/rest/actions/workflow-jobs" + } + }, + "gitlab": { + "name": "GitLab rolling documentation", + "basis": "unknown", + "sources": { + "seb30e3937183": "https://docs.gitlab.com/runner/security/", + "sde661ff4762e": "https://docs.gitlab.com/runner/executors/docker_autoscaler/", + "s265254bc5592": "https://docs.gitlab.com/runner/executors/docker/", + "s47b236d63dc4": "https://docs.gitlab.com/runner/configuration/advanced-configuration/", + "s1579c499c75b": "https://docs.gitlab.com/runner/executors/kubernetes/", + "sd2cbff5d5d6e": "https://docs.gitlab.com/ci/runners/new_creation_workflow/", + "se430cbabc011": "https://docs.gitlab.com/api/users/#create-a-runner-linked-to-a-user", + "saef0483b7be7": "https://docs.gitlab.com/ci/jobs/ci_job_token/", + "s699c1c198119": "https://docs.gitlab.com/api/project_job_token_scopes/", + "sbc009ff2866a": "https://docs.gitlab.com/runner/monitoring/" + } + }, + "docker": { + "name": "Docker Engine documentation", + "basis": "unknown", + "sources": { + "s18f59818e451": "https://docs.docker.com/engine/security/" + } + }, + "python": { + "name": "Python tomllib minimum", + "basis": "3.11", + "sources": { + "s37be9e98ea29": "https://docs.python.org/3.11/library/tomllib.html" + } + }, + "aws": { + "name": "AWS EC2 documentation", + "basis": "unknown", + "sources": { + "s82aca706445c": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html" + } + } + }, + "claims": { + "job-transport": {"text": "Both runners poll over HTTPS without an inbound job-polling port; verify GitLab TLS and restrict separate autoscaler management or webhook listeners.", "components": ["github", "gitlab"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "gitlab:sde661ff4762e"], "status": "REASONED"}, + "worker-account": {"text": "Use a dedicated least-privileged account, keep provisioning credentials off workers and confine rootful Docker access to disposable VMs.", "components": ["github", "gitlab", "docker"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "docker:s18f59818e451"], "status": "REASONED"}, + "worker-egress": {"text": "Enforce job egress outside job control, isolate production and other workers, and deny unused metadata over both families; IMDSv2 does not stop arbitrary authorized job code.", "components": ["github", "gitlab", "aws"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "aws:s82aca706445c"], "status": "REASONED"}, + "github-eligibility": {"text": "Keep self-hosted runners off public repositories; runner groups admit only private repositories by default and the public override should stay off.", "components": ["github"], "sources": ["github:s92e8e4820392", "github:s215dc07e3642"], "status": "REASONED"}, + "fork-token": {"text": "Ordinary fork pull_request jobs normally receive a read-only GITHUB_TOKEN and no other secrets, but can still read host credentials and reachable services.", "components": ["github"], "sources": ["github:s215dc07e3642", "github:sbbcb15f795a8"], "status": "REASONED"}, + "privileged-triggers": {"text": "pull_request_target and workflow_run can access secrets and write tokens; never execute untrusted PR code, artifacts or caches in these jobs.", "components": ["github"], "sources": ["github:sbbcb15f795a8", "github:s215dc07e3642"], "status": "REASONED"}, + "private-forks": {"text": "Review private-repository fork settings, which can grant write tokens and secrets; separate trusted privileged workers from untrusted CI.", "components": ["github"], "sources": ["github:s215dc07e3642"], "status": "REASONED"}, + "registration-token": {"text": "Registration tokens last one hour; generate on demand, never bake into images, and do not confuse expiry with runner deregistration.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, + "registration-manager": {"text": "Keep the App/PAT generating registration or JIT credentials off workers; organization endpoints need Self-hosted runners: write, repository endpoints Administration: write.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, + "jit": {"text": "Protect encoded_jit_config and the whole response; JIT runners execute at most one job.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, + "runner-credential": {"text": "Protect the installed runner authentication credential, directory and backups.", "components": ["github"], "sources": ["github:s215dc07e3642"], "status": "REASONED"}, + "github-job-token": {"text": "GITHUB_TOKEN is repository-scoped and per-job; set read-only defaults and grant only trusted jobs additional permissions.", "components": ["github"], "sources": ["github:s4561d20e7042"], "status": "REASONED"}, + "oidc": {"text": "Grant id-token: write only where needed; the cloud trust must restrict issuer, audience and repository/branch or protected-environment subject.", "components": ["github"], "sources": ["github:s80fe11d8c90d"], "status": "REASONED"}, + "github-lifecycle": {"text": "Prefer ephemeral/JIT workers and destroy their machines and storage; deregistration does not destroy disks, and labels route rather than authorize jobs.", "components": ["github", "github-api"], "sources": ["github:s215dc07e3642", "github:s92e8e4820392", "github-api:s8217448953a4"], "status": "REASONED"}, + "shell-executor": {"text": "The GitLab shell executor runs as the runner user without isolation; avoid it for untrusted jobs on shared hosts.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183"], "status": "REASONED"}, + "docker-executor": {"text": "Use unprivileged Docker; privileged=true or services_privileged and host Docker socket/TCP access can expose the host despite privileged=false.", "components": ["gitlab", "docker"], "sources": ["gitlab:s265254bc5592", "gitlab:seb30e3937183", "docker:s18f59818e451"], "status": "REASONED"}, + "executor-host-access": {"text": "Deny sensitive host mounts, host PID/network namespaces, unnecessary devices and capabilities; build without the host daemon.", "components": ["gitlab"], "sources": ["gitlab:s265254bc5592", "gitlab:s47b236d63dc4"], "status": "REASONED"}, + "kubernetes-executor": {"text": "Enforce nonprivileged containers, no privilege escalation, scoped ServiceAccounts and no unnecessary token mounting; prevent job overrides.", "components": ["gitlab"], "sources": ["gitlab:s1579c499c75b"], "status": "REASONED"}, + "gitlab-disposal": {"text": "Containers share the kernel; use disposable separate VMs/nodes as needed and one-job autoscaling workers. Instance and Docker Autoscaler GA is recorded as Runner 17.1.", "components": ["gitlab"], "sources": ["gitlab:sde661ff4762e", "gitlab:seb30e3937183"], "status": "REASONED"}, + "gitlab-registration": {"text": "Legacy registration tokens were disabled by default in GitLab 17.0, with removal scheduled for 20.0; use runner authentication tokens.", "components": ["gitlab"], "sources": ["gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, + "gitlab-runner-token": {"text": "Protect and rotate the runner authentication token in config.toml and backups; a cloned runner can steal assigned jobs.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, + "gitlab-creation": {"text": "POST /user/runners needs create_runner scope; keep that access token off workers and set scope/protection at creation.", "components": ["gitlab"], "sources": ["gitlab:se430cbabc011", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, + "gitlab-job-token": {"text": "CI_JOB_TOKEN carries supported triggering-user access during the job and appears as CI_REGISTRY_PASSWORD and inside CI_REPOSITORY_URL.", "components": ["gitlab"], "sources": ["gitlab:saef0483b7be7"], "status": "REASONED"}, + "gitlab-allowlist": {"text": "Target-project inbound job-token allowlists limit who may access that target, not job egress; also restrict resource feature visibility.", "components": ["gitlab"], "sources": ["gitlab:saef0483b7be7", "gitlab:s699c1c198119"], "status": "REASONED"}, + "gitlab-forks": {"text": "Instance runners on public installations accept strangers; project scope does not establish code trust. Review parent-launched fork pipelines or disable ci_allow_fork_pipelines_to_run_in_parent_project.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183"], "status": "REASONED"}, + "gitlab-protected": {"text": "Developer access can compromise persistent runners; release runners need ref_protected and a pool separate from fork-merge-request testing.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, + "gitlab-metrics": {"text": "Optional listen_address metrics and profiling have no built-in authorization; 9252 is allocated, not proof of the configured port.", "components": ["gitlab"], "sources": ["gitlab:sbc009ff2866a"], "status": "REASONED"}, + "gitlab-session": {"text": "Optional session_server uses TLS and must admit required GitLab traffic at its configured address; 8093 is an example, not an established default.", "components": ["gitlab"], "sources": ["gitlab:s47b236d63dc4"], "status": "REASONED"}, + "verify-eligibility": {"text": "Fully paginate groups, selected repositories and public repositories' own runners; visibility=all alone is not public eligibility, and inherited access needs review.", "components": ["github-api"], "sources": ["github-api:sd36db9ea1031", "github-api:s8217448953a4"], "status": "REASONED", "verify": [1]}, + "verify-permissions": {"text": "Inspect organization and repository workflow-token defaults and PR approval settings; explicit workflow permissions can raise grants and id-token is separate.", "components": ["github"], "sources": ["github:s4561d20e7042", "github:s80fe11d8c90d"], "status": "REASONED", "verify": [2]}, + "verify-assignment": {"text": "A controlled fork workflow should run when exposed and lose assignment when restricted, while an allowed private job still runs; skipped/pending/offline/timeout is inconclusive.", "components": ["github-api", "github"], "sources": ["github-api:sbeee4a47110c", "github:s92e8e4820392"], "status": "REASONED", "verify": [3]}, + "verify-config": {"text": "Python 3.14.4 synthetic fixtures reported exposed/fixed Docker, Kubernetes and autoscaler values; wrong types passed through, misplaced keys printed null, and empty/error cases differed in output and exit status.", "components": ["python", "gitlab"], "sources": ["python:s37be9e98ea29", "gitlab:s47b236d63dc4", "gitlab:s1579c499c75b"], "status": "DEMONSTRATED", "verify": [4], "evidence": "the extracted Python program ran without sudo on 20 synthetic config.toml cases using Python 3.14.4. Docker fixtures printed true/true, a socket mount and a TCP host versus false/false, empty volumes and a null host; both exited 0. Autoscaler limits printed null/null, 1/2 and 2/1 versus 1/1, all exit 0. Kubernetes fixtures printed true versus false for the three booleans and the supplied service_account. Wrong-type strings, booleans and floats retained their JSON types; misplaced privileged printed docker.privileged null. Empty and runner-less files printed nothing and exited 0. Missing files, malformed TOML and wrong section types exited 1; the second-runner structure error followed one printed runner. Two valid runners printed indices 0 and 1. The runner token key was omitted; a dummy token copied into service_account was printed. These observations cover file inspection only."}, + "verify-daemon": {"text": "Resolve docker.host through DOCKER_HOST or the default socket when absent; flag actual job access to a rootful daemon, not tcp:// alone.", "components": ["gitlab", "docker"], "sources": ["gitlab:s265254bc5592", "docker:s18f59818e451"], "status": "REASONED"}, + "verify-autoscaler": {"text": "For Instance/Docker Autoscaler require capacity_per_instance=1 and max_use_count=1 explicitly as integers; absent or other values are findings.", "components": ["gitlab"], "sources": ["gitlab:sde661ff4762e"], "status": "REASONED"}, + "verify-gitlab-api": {"text": "Read inbound allowlists and require release-runner ref_protected against exposed controls; gitlab-runner verify proves authentication only.", "components": ["gitlab"], "sources": ["gitlab:s699c1c198119", "gitlab:seb30e3937183"], "status": "REASONED", "verify": [5]}, + "verify-lifecycle": {"text": "Check complete deregistration listings and infrastructure disposal; persistent-worker canary must survive in the control, but absence alone does not prove destruction or clean shared caches.", "components": ["github-api", "gitlab", "github"], "sources": ["github-api:s8217448953a4", "gitlab:sde661ff4762e", "github:s215dc07e3642"], "status": "REASONED"}, + "verify-listeners": {"text": "Inventory namespaces and publications, require the permitted-client response, then test disallowed access; any HTTP response proves reachability, failure needs corroborating firewall denial.", "components": ["gitlab"], "sources": ["gitlab:sbc009ff2866a", "gitlab:s47b236d63dc4"], "status": "REASONED", "verify": [6]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/self-signed.md b/plugin/skills/secure-deployment-config/references/self-signed.md index d2b5dac7f..4b9bda0d6 100644 --- a/plugin/skills/secure-deployment-config/references/self-signed.md +++ b/plugin/skills/secure-deployment-config/references/self-signed.md @@ -1,5 +1,203 @@ ---- -version_basis: { +# Self-signed certificates + +Written against OpenSSL documentation (rolling) (no recorded version), Requests TLS documentation v2.32.3, Git ignore documentation v2.43.0, curl command documentation curl-8_12_1, Node.js extra CA certificates v22.19.0, RFC 9525 service identity RFC 9525, Debian ca-certificates trixie, and Ubuntu ca-certificates noble, plus 3 more recorded bases; 1 further source carries no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 24 claims: 1 demonstrated by a check that ran and 23 reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + +Use a self-signed certificate when the service has no public DNS name, when an ACME CA cannot reach it, and when [cloudflare.md](cloudflare.md) is not an option: internal tools, lab and development environments, and machine-to-machine links on private networks. For anything a browser user or external party reaches, prefer [free-certificates.md](free-certificates.md); self-signed certificates trigger browser warnings and every client must be configured to trust them. + +Self-signed TLS still matters. It encrypts credentials and data in transit; without it, authentication tokens cross the network in cleartext. + +## 1. Generate a certificate with OpenSSL + +RSA, single command (OpenSSL 3.0 or later): + +```bash +openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -noenc \ + -keyout server.key -out server.crt \ + -subj "/CN=app.internal" \ + -addext "basicConstraints=critical,CA:FALSE" \ + -addext "subjectAltName=DNS:app.internal,DNS:localhost,IP:127.0.0.1,IP:203.0.113.10" +``` + +ECDSA (smaller and faster; generate the key first, then the certificate): + +```bash +openssl ecparam -name prime256v1 -genkey -noout -out server.key +openssl req -x509 -key server.key -sha256 -days 365 -out server.crt \ + -subj "/CN=app.internal" \ + -addext "basicConstraints=critical,CA:FALSE" \ + -addext "subjectAltName=DNS:app.internal,IP:203.0.113.10" +``` + +Rules that make the certificate actually work: + +- The `subjectAltName` list must contain every DNS name and IP address clients will use to reach the service (this guide's requirement, based on the RFC 9525 matching rules that follow). RFC 9525 requires DNS identity matching by comparing labels ('Each label MUST match', except under its wildcard rule) and says an IP-ID reference identifier 'MUST exactly match the value of an iPAddress entry in subjectAltName'. It says the subject Common Name 'MUST NOT be used to identify a service'. If no presented identifier matches, RFC 9525 says an automated client 'SHOULD terminate' and permits a configured exception that 'MUST NOT' disable this security control by default, while a human-facing client 'SHOULD' warn and terminate but may offer advanced users an override. Its ad hoc pinning provision says such pinning 'SHOULD NOT' restrict future connections to only the pinned certificate. Some verifiers, including OpenSSL's default, still fall back to the CN, so confirm in Verify below that the SAN exists rather than trusting a CN. Replace the example `203.0.113.10` (a documentation address from RFC 5737) with your service's real IP, or drop the `IP:` entries if clients reach it only by name. +- `basicConstraints=critical,CA:FALSE` marks the certificate a leaf, not a CA. It matters because section 4 installs the certificate into client trust stores: OpenSSL's stock configuration marks a bare `req -x509` certificate `CA:TRUE`, which in a trust store is a signer for any name, using the unencrypted key that `-noenc` leaves on the server. This override relies on OpenSSL 3.0 or later, where a command-line extension replaces the configuration default; the end-of-life 1.1.1 would instead emit a duplicate, invalid extension. +- `-noenc` (spelled `-nodes` before OpenSSL 3.0, still accepted but deprecated) leaves the key unencrypted so services can start unattended; protect it with file permissions instead. +- Track the `-days` expiry. Nothing renews a self-signed certificate for you; put the date in your calendar or monitoring. + +## 2. Protect the private key + +This guide advises protecting `server.key` with mode `600` and ownership by the account and group your service runs as; `app:app` below is this guide's example, to replace with that account and group. GNU coreutils v9.7 documents `chmod` changing file access permissions and taking new mode bits, and `chown owner:group` changing both owner and group (see Sources); it does not recommend a service account or select one for this key. This guide's reading of `600` is owner read/write and no group or other permissions, based on GNU coreutils v9.7's Numeric Modes definition in `doc/perm.texi` (see Sources). + +```bash +chmod 600 server.key +chown app:app server.key # whichever user your service runs as +``` + +Never commit a private key to version control. Add `*.key` and `*.pem` to `.gitignore` before generating anything inside a repository, and treat any key that has ever been committed or pasted into a chat as compromised: regenerate it. + +## 3. mkcert for local development + +[mkcert](https://github.com/FiloSottile/mkcert) creates a local CA, installs it into your OS and browser trust stores, and issues certificates that your own machine trusts with no warnings: + +```bash +mkcert -install +mkcert app.test localhost 127.0.0.1 ::1 +``` + +This is for development machines only. The generated CA can sign for any name, so its key must never leave the developer's machine, and mkcert certificates must never serve real users. + +## 4. Make clients trust the certificate; never disable verification + +Distribute the certificate (or your internal CA certificate) to clients instead of turning verification off: + +```bash +# Debian trixie/Ubuntu noble system trust store; other releases: this guide's assessment +sudo cp server.crt /usr/local/share/ca-certificates/app-internal.crt +sudo update-ca-certificates + +# RHEL 9 system trust store; Fedora and other RHEL releases: this guide's assessment +sudo cp server.crt /etc/pki/ca-trust/source/anchors/app-internal.crt +sudo update-ca-trust extract + +# Per-tool +curl -q --cacert server.crt https://app.internal/ +export REQUESTS_CA_BUNDLE=/path/to/server.crt # Python requests +export NODE_EXTRA_CA_CERTS=/path/to/server.crt # Node.js +``` + +Do not ship `curl -k`, `verify=False`, `rejectUnauthorized: false`, or `NODE_TLS_REJECT_UNAUTHORIZED=0` in committed code. For curl and Requests, `-k` and `verify=False` disable certificate verification (see their existing Sources entries). Node.js v24.22.0 documents `NODE_TLS_REJECT_UNAUTHORIZED=0` as an environment variable disabling certificate validation for TLS connections in the Node.js process and strongly discourages its use. In `tls.connect`, `rejectUnauthorized: false` disables verification of the server certificate against supplied CAs for that client connection. In `tls.createServer`, it disables rejection of connections unauthorized by supplied CAs; the option only has an effect when `requestCert` is `true`. These Node.js passages establish the documented settings and their scope, not their use in a particular deployment. This guide's assessment is that these bypasses accept attacker-controlled certificates as readily as your own and reliably survive into production. + +## 5. Verify + +DEMONSTRATED: following block; local certificate-output inspection only. On 2026-10-05 in private /dev/shm scratch, OpenSSL 3.5.5 ran the printed x509 command on three disposable one-day RSA certificates. Missing-SAN and CA:TRUE fixtures each exited 0 but lacked the required SAN or leaf constraint; the fixed fixture also exited 0 and printed DNS:app.internal, IP Address:127.0.0.1 and critical CA:FALSE. Subject and validity dates printed for all three. + +```bash +openssl x509 -in server.crt -noout -subject -dates -ext subjectAltName,basicConstraints +``` + +REASONED: following block; live TLS identity, failure handling and client-trust checks follow the cited OpenSSL and mkcert documentation. No app.internal service or client trust-store installation was supplied for these checks. The local x509 run above does not demonstrate a handshake or installed trust; expected outcomes and limitations are stated below. + +```bash +openssl s_client -connect app.internal:443 -servername app.internal -verify_hostname app.internal -verify_return_error -CAfile server.crt ` to check an IP identity, since `-verify_hostname` falls back to the Common Name whenever the certificate carries no DNS-name SAN entry (even if it has an IP SAN), so a CN-only or IP-only certificate can still pass a hostname check. The `s_client` check supplies trust explicitly with `-CAfile`, so it proves the certificate and name but not that the section-4 trust-store install took effect; confirm that separately from each client with its normal configuration (for the system store, a plain `curl https://app.internal/` with no `--cacert` should fail before `update-ca-certificates` and succeed after). + +## Limits to plan around + +Self-signed certificates have no revocation and no third-party accountability, and trust distribution is manual. When the audience grows beyond a small internal group, move to [free-certificates.md](free-certificates.md) or put the service behind [cloudflare.md](cloudflare.md). Authentication is still required either way: see [authentication.md](authentication.md). + +## Sources (checked September 2026) + +- OpenSSL documentation (rolling documentation, checked September 2026): https://docs.openssl.org/ ; `openssl s_client` (`-servername`, `-verify_return_error`): https://docs.openssl.org/master/man1/openssl-s_client/ ; verification options (`-verify_hostname`): https://docs.openssl.org/master/man1/openssl-verification-options/ +- mkcert: https://github.com/FiloSottile/mkcert +- Requests v2.32.3 CA-bundle environment variables (checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L238-L239 +- Requests v2.32.3 disabled certificate verification (checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L246-L250 +- Git v2.43.0 ignore rules and already tracked files (checked October 2026): https://github.com/git/git/blob/v2.43.0/Documentation/gitignore.txt#L15-L18 +- curl curl-8_12_1 explicit CA trust (checked October 2026): https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/cacert.md#L21-L24 +- curl curl-8_12_1 normal certificate verification and `--insecure` (checked October 2026): https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/insecure.md#L21-L28 +- Node.js v22.19.0 extra trusted CA certificates (checked October 2026): https://github.com/nodejs/node/blob/v22.19.0/doc/api/cli.md#L3234-L3238 +- RFC 9525 subject Common Name (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-2 +- RFC 9525 reference identifiers and IP-ID matching (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.2 +- RFC 9525 DNS identity matching (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.3 +- RFC 9525 IP identity matching (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.4 +- RFC 9525 identity mismatch outcome (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.6 +- Debian trixie update-ca-certificates (checked October 2026): https://manpages.debian.org/trixie/ca-certificates/update-ca-certificates.8.en.html#DESCRIPTION +- Ubuntu noble update-ca-certificates (checked October 2026): https://manpages.ubuntu.com/manpages/noble/man8/update-ca-certificates.8.html#description +- RHEL 9 shared system certificates (checked October 2026): https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/securing_networks/using-shared-system-certificates_securing-networks#adding-new-certificates_using-shared-system-certificates +- GNU coreutils v9.7 `chmod` syntax (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11937-L11942 +- GNU coreutils v9.7 mode argument (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11967-L11968 +- GNU coreutils v9.7 `chown` owner and group (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11614-L11622 +- GNU coreutils v9.7 numeric mode definition (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/perm.texi#L492-L535 +- Node.js v24.22.0 `NODE_TLS_REJECT_UNAUTHORIZED` (checked October 2026): https://github.com/nodejs/node/blob/v24.22.0/doc/api/cli.md#L3940-L3944 +- Node.js v24.22.0 `tls.connect` client `rejectUnauthorized` (checked October 2026): https://github.com/nodejs/node/blob/v24.22.0/doc/api/tls.md#L1786-L1789 +- Node.js v24.22.0 `tls.createServer` `rejectUnauthorized` and `requestCert` (checked October 2026): https://github.com/nodejs/node/blob/v24.22.0/doc/api/tls.md#L2250-L2255 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/self-signed.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- openssl: OpenSSL documentation (rolling) (unknown) +- mkcert: mkcert (unknown) +- requests: Requests TLS documentation (v2.32.3) +- git: Git ignore documentation (v2.43.0) +- curl-source: curl command documentation (curl-8_12_1) +- node: Node.js extra CA certificates (v22.19.0) +- rfc9525: RFC 9525 service identity (RFC 9525) +- debian-ca: Debian ca-certificates (trixie) +- ubuntu-ca: Ubuntu ca-certificates (noble) +- rhel-ca: RHEL shared system certificates (RHEL 9) +- coreutils: GNU coreutils file permissions (v9.7) +- node-v24: Node.js API documentation (v24.22.0) + +Claims (full source mapping in the JSON block below): + +- trust-scope (REASONED; openssl, mkcert): Self-signed TLS encrypts transit but needs explicit client trust and triggers browser warnings; the guide prefers publicly trusted certificates for browser users and external parties. +- rsa-certificate (REASONED; openssl): OpenSSL 3.0 or later generates an RSA 4096-bit self-signed certificate with SHA-256, 365-day validity, an unencrypted key and the shown subject and extensions. +- ec-certificate (REASONED; openssl): Generate a prime256v1 EC key first, then a SHA-256 self-signed certificate with 365-day validity and the shown subject and extensions; the guide describes ECDSA as smaller and faster. +- san (REASONED; openssl, rfc9525): SAN must cover every client DNS name and IP (this guide's requirement, based on RFC 9525's matching rules); replace the documentation IP or omit IP entries for name-only access. RFC 9525 requires DNS identity matching label by label except under its wildcard rule; an IP-ID reference identifier MUST exactly match an iPAddress subjectAltName entry. It forbids the subject Common Name as a service identifier. On mismatch, an automated client SHOULD terminate; a configured exception MUST NOT disable this control by default. A human-facing client SHOULD warn and terminate but MAY offer advanced users an override. Ad hoc pinning SHOULD NOT restrict future connections to only the pinned certificate. +- leaf-constraint (REASONED; openssl): Set basicConstraints=critical,CA:FALSE because the stated stock req -x509 configuration otherwise marks a trusted certificate CA:TRUE, allowing its unencrypted server key to sign for any name. +- extension-history (REASONED; openssl): The command-line extension override relies on OpenSSL 3.0 or later; the guide says end-of-life 1.1.1 instead emits a duplicate invalid extension. +- unencrypted-key (REASONED; openssl): -noenc leaves the key unencrypted for unattended startup; -nodes is the pre-3.0 spelling, still accepted but deprecated. Protect the key with permissions. +- expiry (REASONED; openssl): Track the -days expiry with a calendar or monitoring; self-signed certificates do not renew themselves. +- key-permissions (REASONED; openssl, coreutils): This guide advises chmod 600 and service-user ownership for server.key; GNU coreutils v9.7 documents chmod's mode changes and chown's owner/group changes, not that advice. Mode 600's owner read/write and no group/other permissions are this guide's reading of GNU coreutils v9.7's Numeric Modes definition in doc/perm.texi. +- key-compromise (REASONED; openssl, git): Never commit private keys; ignore *.key and *.pem before generation in a repository and regenerate keys committed or pasted into chat. +- mkcert (REASONED; mkcert): mkcert -install creates and trusts a local CA; mkcert issues certificates for the listed development DNS names and IPs. +- mkcert-custody (REASONED; mkcert): Use mkcert only for development; its CA key can sign for any name, must not leave the developer's machine and must not serve real users. +- debian-trust (REASONED; mkcert, debian-ca, ubuntu-ca): Install app-internal.crt under /usr/local/share/ca-certificates and run update-ca-certificates on Debian trixie and Ubuntu noble; use on other Debian and Ubuntu releases is this guide's assessment, with platform-specific Sources entries for those releases absent. +- rhel-trust (REASONED; mkcert, rhel-ca): Install app-internal.crt under /etc/pki/ca-trust/source/anchors and run update-ca-trust extract on RHEL 9; Fedora and other RHEL releases are this guide's assessment, with platform-specific Sources entries for those releases absent. +- curl-trust (REASONED; openssl, curl-source): curl --cacert server.crt supplies explicit trust for app.internal. +- requests-trust (REASONED; openssl, requests): REQUESTS_CA_BUNDLE supplies the certificate path to Python requests. +- node-trust (REASONED; mkcert, node): NODE_EXTRA_CA_CERTS supplies additional client trust to Node.js. +- verification-bypasses (REASONED; openssl, requests, curl-source, node-v24): Do not commit curl -k, verify=False, rejectUnauthorized: false or NODE_TLS_REJECT_UNAUTHORIZED=0. Node.js v24.22.0 scopes the bypasses to process-environment TLS validation, client-connection server verification or server rejection with requestCert=true; accepting attacker certificates and persistence into production are this guide's assessment. +- verify-extensions (DEMONSTRATED; openssl): Inspect x509 output for SAN coverage and CA:FALSE, not its exit status: it can exit 0 with no SAN. +- verify-tls (REASONED; openssl): The app.internal:443 s_client check supplies SNI, hostname checking, verification errors and CAfile; a pass prints Verification: OK, Verified peername: app.internal and return code 0, and exits 0. +- verify-failures (REASONED; openssl): Without -verify_hostname the name is ignored; without -verify_return_error a verification failure can still complete the handshake and exit 0. Both flags make name or trust failures exit nonzero; wrong name reports code 62. +- verify-identity (REASONED; openssl): Use -verify_ip for IP identities; -verify_hostname can fall back to CN without a DNS SAN, even with an IP SAN, so CN-only or IP-only certificates can pass hostname checks. +- verify-store (REASONED; openssl, mkcert, curl-source): Explicit -CAfile proves certificate and name, not the system-store installation; test each normally configured client separately, expecting plain curl without --cacert to fail before trust installation and succeed after. +- trust-limits (REASONED; openssl, mkcert): Self-signed certificates have no revocation or third-party accountability and need manual trust distribution; move beyond them as the audience grows and retain authentication. + +````json +{ "schema": 1, "checked": "2026-10-19", "documentation_checked": "2026-09", @@ -130,177 +328,5 @@ version_basis: { "trust-limits": {"text": "Self-signed certificates have no revocation or third-party accountability and need manual trust distribution; move beyond them as the audience grows and retain authentication.", "components": ["openssl", "mkcert"], "sources": ["openssl:s211b1e6e4aa4", "mkcert:s41ae17dba4ce"], "status": "REASONED"} } } ---- -# Self-signed certificates - - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- openssl: OpenSSL documentation (rolling) (unknown) -- mkcert: mkcert (unknown) -- requests: Requests TLS documentation (v2.32.3) -- git: Git ignore documentation (v2.43.0) -- curl-source: curl command documentation (curl-8_12_1) -- node: Node.js extra CA certificates (v22.19.0) -- rfc9525: RFC 9525 service identity (RFC 9525) -- debian-ca: Debian ca-certificates (trixie) -- ubuntu-ca: Ubuntu ca-certificates (noble) -- rhel-ca: RHEL shared system certificates (RHEL 9) -- coreutils: GNU coreutils file permissions (v9.7) -- node-v24: Node.js API documentation (v24.22.0) - -Claims (full source mapping in the front matter): - -- trust-scope (REASONED; openssl, mkcert): Self-signed TLS encrypts transit but needs explicit client trust and triggers browser warnings; the guide prefers publicly trusted certificates for browser users and external parties. -- rsa-certificate (REASONED; openssl): OpenSSL 3.0 or later generates an RSA 4096-bit self-signed certificate with SHA-256, 365-day validity, an unencrypted key and the shown subject and extensions. -- ec-certificate (REASONED; openssl): Generate a prime256v1 EC key first, then a SHA-256 self-signed certificate with 365-day validity and the shown subject and extensions; the guide describes ECDSA as smaller and faster. -- san (REASONED; openssl, rfc9525): SAN must cover every client DNS name and IP (this guide's requirement, based on RFC 9525's matching rules); replace the documentation IP or omit IP entries for name-only access. RFC 9525 requires DNS identity matching label by label except under its wildcard rule; an IP-ID reference identifier MUST exactly match an iPAddress subjectAltName entry. It forbids the subject Common Name as a service identifier. On mismatch, an automated client SHOULD terminate; a configured exception MUST NOT disable this control by default. A human-facing client SHOULD warn and terminate but MAY offer advanced users an override. Ad hoc pinning SHOULD NOT restrict future connections to only the pinned certificate. -- leaf-constraint (REASONED; openssl): Set basicConstraints=critical,CA:FALSE because the stated stock req -x509 configuration otherwise marks a trusted certificate CA:TRUE, allowing its unencrypted server key to sign for any name. -- extension-history (REASONED; openssl): The command-line extension override relies on OpenSSL 3.0 or later; the guide says end-of-life 1.1.1 instead emits a duplicate invalid extension. -- unencrypted-key (REASONED; openssl): -noenc leaves the key unencrypted for unattended startup; -nodes is the pre-3.0 spelling, still accepted but deprecated. Protect the key with permissions. -- expiry (REASONED; openssl): Track the -days expiry with a calendar or monitoring; self-signed certificates do not renew themselves. -- key-permissions (REASONED; openssl, coreutils): This guide advises chmod 600 and service-user ownership for server.key; GNU coreutils v9.7 documents chmod's mode changes and chown's owner/group changes, not that advice. Mode 600's owner read/write and no group/other permissions are this guide's reading of GNU coreutils v9.7's Numeric Modes definition in doc/perm.texi. -- key-compromise (REASONED; openssl, git): Never commit private keys; ignore *.key and *.pem before generation in a repository and regenerate keys committed or pasted into chat. -- mkcert (REASONED; mkcert): mkcert -install creates and trusts a local CA; mkcert issues certificates for the listed development DNS names and IPs. -- mkcert-custody (REASONED; mkcert): Use mkcert only for development; its CA key can sign for any name, must not leave the developer's machine and must not serve real users. -- debian-trust (REASONED; mkcert, debian-ca, ubuntu-ca): Install app-internal.crt under /usr/local/share/ca-certificates and run update-ca-certificates on Debian trixie and Ubuntu noble; use on other Debian and Ubuntu releases is this guide's assessment, with platform-specific Sources entries for those releases absent. -- rhel-trust (REASONED; mkcert, rhel-ca): Install app-internal.crt under /etc/pki/ca-trust/source/anchors and run update-ca-trust extract on RHEL 9; Fedora and other RHEL releases are this guide's assessment, with platform-specific Sources entries for those releases absent. -- curl-trust (REASONED; openssl, curl-source): curl --cacert server.crt supplies explicit trust for app.internal. -- requests-trust (REASONED; openssl, requests): REQUESTS_CA_BUNDLE supplies the certificate path to Python requests. -- node-trust (REASONED; mkcert, node): NODE_EXTRA_CA_CERTS supplies additional client trust to Node.js. -- verification-bypasses (REASONED; openssl, requests, curl-source, node-v24): Do not commit curl -k, verify=False, rejectUnauthorized: false or NODE_TLS_REJECT_UNAUTHORIZED=0. Node.js v24.22.0 scopes the bypasses to process-environment TLS validation, client-connection server verification or server rejection with requestCert=true; accepting attacker certificates and persistence into production are this guide's assessment. -- verify-extensions (DEMONSTRATED; openssl): Inspect x509 output for SAN coverage and CA:FALSE, not its exit status: it can exit 0 with no SAN. -- verify-tls (REASONED; openssl): The app.internal:443 s_client check supplies SNI, hostname checking, verification errors and CAfile; a pass prints Verification: OK, Verified peername: app.internal and return code 0, and exits 0. -- verify-failures (REASONED; openssl): Without -verify_hostname the name is ignored; without -verify_return_error a verification failure can still complete the handshake and exit 0. Both flags make name or trust failures exit nonzero; wrong name reports code 62. -- verify-identity (REASONED; openssl): Use -verify_ip for IP identities; -verify_hostname can fall back to CN without a DNS SAN, even with an IP SAN, so CN-only or IP-only certificates can pass hostname checks. -- verify-store (REASONED; openssl, mkcert, curl-source): Explicit -CAfile proves certificate and name, not the system-store installation; test each normally configured client separately, expecting plain curl without --cacert to fail before trust installation and succeed after. -- trust-limits (REASONED; openssl, mkcert): Self-signed certificates have no revocation or third-party accountability and need manual trust distribution; move beyond them as the audience grows and retain authentication. +```` - -Use a self-signed certificate when the service has no public DNS name, when an ACME CA cannot reach it, and when [cloudflare.md](cloudflare.md) is not an option: internal tools, lab and development environments, and machine-to-machine links on private networks. For anything a browser user or external party reaches, prefer [free-certificates.md](free-certificates.md); self-signed certificates trigger browser warnings and every client must be configured to trust them. - -Self-signed TLS still matters. It encrypts credentials and data in transit; without it, authentication tokens cross the network in cleartext. - -## 1. Generate a certificate with OpenSSL - -RSA, single command (OpenSSL 3.0 or later): - -```bash -openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -noenc \ - -keyout server.key -out server.crt \ - -subj "/CN=app.internal" \ - -addext "basicConstraints=critical,CA:FALSE" \ - -addext "subjectAltName=DNS:app.internal,DNS:localhost,IP:127.0.0.1,IP:203.0.113.10" -``` - -ECDSA (smaller and faster; generate the key first, then the certificate): - -```bash -openssl ecparam -name prime256v1 -genkey -noout -out server.key -openssl req -x509 -key server.key -sha256 -days 365 -out server.crt \ - -subj "/CN=app.internal" \ - -addext "basicConstraints=critical,CA:FALSE" \ - -addext "subjectAltName=DNS:app.internal,IP:203.0.113.10" -``` - -Rules that make the certificate actually work: - -- The `subjectAltName` list must contain every DNS name and IP address clients will use to reach the service (this guide's requirement, based on the RFC 9525 matching rules that follow). RFC 9525 requires DNS identity matching by comparing labels ('Each label MUST match', except under its wildcard rule) and says an IP-ID reference identifier 'MUST exactly match the value of an iPAddress entry in subjectAltName'. It says the subject Common Name 'MUST NOT be used to identify a service'. If no presented identifier matches, RFC 9525 says an automated client 'SHOULD terminate' and permits a configured exception that 'MUST NOT' disable this security control by default, while a human-facing client 'SHOULD' warn and terminate but may offer advanced users an override. Its ad hoc pinning provision says such pinning 'SHOULD NOT' restrict future connections to only the pinned certificate. Some verifiers, including OpenSSL's default, still fall back to the CN, so confirm in Verify below that the SAN exists rather than trusting a CN. Replace the example `203.0.113.10` (a documentation address from RFC 5737) with your service's real IP, or drop the `IP:` entries if clients reach it only by name. -- `basicConstraints=critical,CA:FALSE` marks the certificate a leaf, not a CA. It matters because section 4 installs the certificate into client trust stores: OpenSSL's stock configuration marks a bare `req -x509` certificate `CA:TRUE`, which in a trust store is a signer for any name, using the unencrypted key that `-noenc` leaves on the server. This override relies on OpenSSL 3.0 or later, where a command-line extension replaces the configuration default; the end-of-life 1.1.1 would instead emit a duplicate, invalid extension. -- `-noenc` (spelled `-nodes` before OpenSSL 3.0, still accepted but deprecated) leaves the key unencrypted so services can start unattended; protect it with file permissions instead. -- Track the `-days` expiry. Nothing renews a self-signed certificate for you; put the date in your calendar or monitoring. - -## 2. Protect the private key - -This guide advises protecting `server.key` with mode `600` and ownership by the account and group your service runs as; `app:app` below is this guide's example, to replace with that account and group. GNU coreutils v9.7 documents `chmod` changing file access permissions and taking new mode bits, and `chown owner:group` changing both owner and group (see Sources); it does not recommend a service account or select one for this key. This guide's reading of `600` is owner read/write and no group or other permissions, based on GNU coreutils v9.7's Numeric Modes definition in `doc/perm.texi` (see Sources). - -```bash -chmod 600 server.key -chown app:app server.key # whichever user your service runs as -``` - -Never commit a private key to version control. Add `*.key` and `*.pem` to `.gitignore` before generating anything inside a repository, and treat any key that has ever been committed or pasted into a chat as compromised: regenerate it. - -## 3. mkcert for local development - -[mkcert](https://github.com/FiloSottile/mkcert) creates a local CA, installs it into your OS and browser trust stores, and issues certificates that your own machine trusts with no warnings: - -```bash -mkcert -install -mkcert app.test localhost 127.0.0.1 ::1 -``` - -This is for development machines only. The generated CA can sign for any name, so its key must never leave the developer's machine, and mkcert certificates must never serve real users. - -## 4. Make clients trust the certificate; never disable verification - -Distribute the certificate (or your internal CA certificate) to clients instead of turning verification off: - -```bash -# Debian trixie/Ubuntu noble system trust store; other releases: this guide's assessment -sudo cp server.crt /usr/local/share/ca-certificates/app-internal.crt -sudo update-ca-certificates - -# RHEL 9 system trust store; Fedora and other RHEL releases: this guide's assessment -sudo cp server.crt /etc/pki/ca-trust/source/anchors/app-internal.crt -sudo update-ca-trust extract - -# Per-tool -curl -q --cacert server.crt https://app.internal/ -export REQUESTS_CA_BUNDLE=/path/to/server.crt # Python requests -export NODE_EXTRA_CA_CERTS=/path/to/server.crt # Node.js -``` - -Do not ship `curl -k`, `verify=False`, `rejectUnauthorized: false`, or `NODE_TLS_REJECT_UNAUTHORIZED=0` in committed code. For curl and Requests, `-k` and `verify=False` disable certificate verification (see their existing Sources entries). Node.js v24.22.0 documents `NODE_TLS_REJECT_UNAUTHORIZED=0` as an environment variable disabling certificate validation for TLS connections in the Node.js process and strongly discourages its use. In `tls.connect`, `rejectUnauthorized: false` disables verification of the server certificate against supplied CAs for that client connection. In `tls.createServer`, it disables rejection of connections unauthorized by supplied CAs; the option only has an effect when `requestCert` is `true`. These Node.js passages establish the documented settings and their scope, not their use in a particular deployment. This guide's assessment is that these bypasses accept attacker-controlled certificates as readily as your own and reliably survive into production. - -## 5. Verify - -DEMONSTRATED: following block; local certificate-output inspection only. On 2026-10-05 in private /dev/shm scratch, OpenSSL 3.5.5 ran the printed x509 command on three disposable one-day RSA certificates. Missing-SAN and CA:TRUE fixtures each exited 0 but lacked the required SAN or leaf constraint; the fixed fixture also exited 0 and printed DNS:app.internal, IP Address:127.0.0.1 and critical CA:FALSE. Subject and validity dates printed for all three. - -```bash -openssl x509 -in server.crt -noout -subject -dates -ext subjectAltName,basicConstraints -``` - -REASONED: following block; live TLS identity, failure handling and client-trust checks follow the cited OpenSSL and mkcert documentation. No app.internal service or client trust-store installation was supplied for these checks. The local x509 run above does not demonstrate a handshake or installed trust; expected outcomes and limitations are stated below. - -```bash -openssl s_client -connect app.internal:443 -servername app.internal -verify_hostname app.internal -verify_return_error -CAfile server.crt ` to check an IP identity, since `-verify_hostname` falls back to the Common Name whenever the certificate carries no DNS-name SAN entry (even if it has an IP SAN), so a CN-only or IP-only certificate can still pass a hostname check. The `s_client` check supplies trust explicitly with `-CAfile`, so it proves the certificate and name but not that the section-4 trust-store install took effect; confirm that separately from each client with its normal configuration (for the system store, a plain `curl https://app.internal/` with no `--cacert` should fail before `update-ca-certificates` and succeed after). - -## Limits to plan around - -Self-signed certificates have no revocation and no third-party accountability, and trust distribution is manual. When the audience grows beyond a small internal group, move to [free-certificates.md](free-certificates.md) or put the service behind [cloudflare.md](cloudflare.md). Authentication is still required either way: see [authentication.md](authentication.md). - -## Sources (checked September 2026) - -- OpenSSL documentation (rolling documentation, checked September 2026): https://docs.openssl.org/ ; `openssl s_client` (`-servername`, `-verify_return_error`): https://docs.openssl.org/master/man1/openssl-s_client/ ; verification options (`-verify_hostname`): https://docs.openssl.org/master/man1/openssl-verification-options/ -- mkcert: https://github.com/FiloSottile/mkcert -- Requests v2.32.3 CA-bundle environment variables (checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L238-L239 -- Requests v2.32.3 disabled certificate verification (checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L246-L250 -- Git v2.43.0 ignore rules and already tracked files (checked October 2026): https://github.com/git/git/blob/v2.43.0/Documentation/gitignore.txt#L15-L18 -- curl curl-8_12_1 explicit CA trust (checked October 2026): https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/cacert.md#L21-L24 -- curl curl-8_12_1 normal certificate verification and `--insecure` (checked October 2026): https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/insecure.md#L21-L28 -- Node.js v22.19.0 extra trusted CA certificates (checked October 2026): https://github.com/nodejs/node/blob/v22.19.0/doc/api/cli.md#L3234-L3238 -- RFC 9525 subject Common Name (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-2 -- RFC 9525 reference identifiers and IP-ID matching (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.2 -- RFC 9525 DNS identity matching (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.3 -- RFC 9525 IP identity matching (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.4 -- RFC 9525 identity mismatch outcome (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.6 -- Debian trixie update-ca-certificates (checked October 2026): https://manpages.debian.org/trixie/ca-certificates/update-ca-certificates.8.en.html#DESCRIPTION -- Ubuntu noble update-ca-certificates (checked October 2026): https://manpages.ubuntu.com/manpages/noble/man8/update-ca-certificates.8.html#description -- RHEL 9 shared system certificates (checked October 2026): https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/securing_networks/using-shared-system-certificates_securing-networks#adding-new-certificates_using-shared-system-certificates -- GNU coreutils v9.7 `chmod` syntax (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11937-L11942 -- GNU coreutils v9.7 mode argument (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11967-L11968 -- GNU coreutils v9.7 `chown` owner and group (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11614-L11622 -- GNU coreutils v9.7 numeric mode definition (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/perm.texi#L492-L535 -- Node.js v24.22.0 `NODE_TLS_REJECT_UNAUTHORIZED` (checked October 2026): https://github.com/nodejs/node/blob/v24.22.0/doc/api/cli.md#L3940-L3944 -- Node.js v24.22.0 `tls.connect` client `rejectUnauthorized` (checked October 2026): https://github.com/nodejs/node/blob/v24.22.0/doc/api/tls.md#L1786-L1789 -- Node.js v24.22.0 `tls.createServer` `rejectUnauthorized` and `requestCert` (checked October 2026): https://github.com/nodejs/node/blob/v24.22.0/doc/api/tls.md#L2250-L2255 diff --git a/plugin/skills/secure-deployment-config/references/traefik.md b/plugin/skills/secure-deployment-config/references/traefik.md index 481d64dea..71db7b10a 100644 --- a/plugin/skills/secure-deployment-config/references/traefik.md +++ b/plugin/skills/secure-deployment-config/references/traefik.md @@ -1,188 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-05", - "documentation_checked": "2026-09", - "body_sha256": "aac661357effc89cf9b208294d0f255b4fb31c03428609d9756c4fa69347809c", - "components": { - "traefik-docs": { - "name": "Traefik documentation (rolling)", - "basis": "unknown", - "sources": { - "scbc0f2135c78": "https://doc.traefik.io/traefik/" - } - }, - "docker": { - "name": "Traefik Docker provider", - "basis": "unknown", - "sources": { - "se23fe4fe79fa": "https://doc.traefik.io/traefik/reference/install-configuration/providers/docker/" - } - }, - "swarm": { - "name": "Traefik Swarm provider", - "basis": "unknown", - "sources": { - "s256d46f3f558": "https://doc.traefik.io/traefik/reference/install-configuration/providers/swarm/" - } - }, - "middleware": { - "name": "Traefik middleware documentation", - "basis": "unknown", - "sources": { - "sd987195d64dd": "https://doc.traefik.io/traefik/middlewares/http/buffering/", - "sd20dcba7ebca": "https://doc.traefik.io/traefik/middlewares/http/inflightreq/", - "scf1b1e35ef5d": "https://doc.traefik.io/traefik/middlewares/http/ratelimit/" - } - }, - "curl": { - "name": "curl write-out minimum", - "basis": "7.75.0", - "sources": { - "s2b2686afaf41": "https://curl.se/docs/manpage.html" - } - }, - "traefik-v3": { - "name": "Traefik v3 documentation", - "basis": "v3.5", - "sources": { - "sd6ad276040b7": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/entrypoints/", - "s418a6b1ad09b": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/tls/certificate-resolvers/acme/", - "s25d849b2d428": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/tls/tls-options/", - "s3acb3f5b1778": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/providers/others/file/", - "s5d7a6940f1dd": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/middlewares/basicauth/", - "s3cf7d994a56a": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/middlewares/forwardauth/" - } - }, - "traefik-source": { - "name": "Traefik ACME storage source", - "basis": "v3.5.0", - "sources": { - "s0e08b0bdcb35": "https://github.com/traefik/traefik/blob/v3.5.0/pkg/provider/acme/local_store.go#L125" - } - }, - "httpd-source": { - "name": "Apache httpd htpasswd source", - "basis": "2.4.69", - "sources": { - "sf711558bdf3c": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L45", - "s4aa0f7f341fb": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L292-L295", - "s4482c55d79a2": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L382", - "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128" - } - }, - "owasp-source": { - "name": "OWASP Password Storage Cheat Sheet", - "basis": "72865b1e3b5754246a101778dc00b37f9c233373", - "sources": { - "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" - } - }, - "cloudflare-source": { - "name": "Cloudflare Access MFA documentation", - "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", - "sources": { - "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" - } - }, - "traefik-basicauth-source": { - "name": "Traefik BasicAuth source", - "basis": "v3.5.0", - "sources": { - "s5f9a13d720c8": "https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/basic_auth.go#L67-L99" - } - } - }, - "claims": { - "entrypoints": {"text": "For Traefik v2/v3, web :80 redirects to HTTPS websecure :443 and an ACME resolver obtains/renews certificates.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:sd6ad276040b7", "traefik-v3:s418a6b1ad09b"], "status": "REASONED"}, - "acme-storage": {"text": "Persist acme.json across restarts with mode 600 to retain certificates and avoid repeated issuance/rate limits.", "components": ["traefik-docs", "traefik-v3", "traefik-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s418a6b1ad09b", "traefik-source:s0e08b0bdcb35"], "status": "REASONED"}, - "acme-challenges": {"text": "TLS-ALPN requires inbound 443; HTTP-01 uses 80, while DNS supports wildcards without inbound ports.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s418a6b1ad09b"], "status": "REASONED"}, - "docker-default": {"text": "Docker discovery defaults exposedByDefault to true; set false and opt each intended container in with traefik.enable=true.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, - "docker-scope": {"text": "Discovery is daemon-wide, not Compose-project scoped; eligible unlabelled containers get routers unless excluded. Missing usable ports skip service creation.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, - "docker-socket": {"text": "The Docker socket gives daemon control despite :ro; use a restricted socket proxy to limit API access.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, - "tls-floor": {"text": "Dynamic default TLS options set minVersion VersionTLS12.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s25d849b2d428"], "status": "REASONED"}, - "route": {"text": "Docker labels select the app host, websecure, letsencrypt resolver and backend port 3000.", "components": ["traefik-docs", "docker"], "sources": ["traefik-docs:scbc0f2135c78", "docker:se23fe4fe79fa"], "status": "REASONED"}, - "backend-isolation": {"text": "Publish only Traefik 80/443, not the app's 3000, to prevent middleware/TLS bypass.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, - "basic": {"text": "Attach bcrypt basicAuth to the router; Apache httpd 2.4.69 htpasswd -nB defaults to bcrypt cost 5, while -C 12 selects cost 12; OWASP at 72865b1e recommends 10 or more for legacy bcrypt.", "components": ["traefik-docs", "traefik-v3", "httpd-source", "owasp-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s5d7a6940f1dd", "httpd-source:sf711558bdf3c", "httpd-source:s4aa0f7f341fb", "httpd-source:s4482c55d79a2", "httpd-source:s309ecdd34379", "owasp-source:s4531e65b6502"], "status": "REASONED"}, - "compose-hash": {"text": "Double each dollar sign in Compose hash labels; file-provider hashes need no such escaping.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s5d7a6940f1dd"], "status": "REASONED"}, - "file-provider": {"text": "Load dynamic files with providers.file.directory/filename and reference their middleware from Docker as app-auth@file.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s3acb3f5b1778"], "status": "REASONED"}, - "mfa": {"text": "Basic is single-factor; use forwardAuth with Authelia/oauth2-proxy or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization).", "components": ["traefik-docs", "traefik-v3", "cloudflare-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s3cf7d994a56a", "cloudflare-source:sdcac732541da"], "status": "REASONED"}, - "body-limit": {"text": "Attach buffering.maxRequestBodyBytes=10485760 alongside existing auth/routing labels; it bounds accepted body size.", "components": ["middleware"], "sources": ["middleware:sd987195d64dd"], "status": "REASONED"}, - "buffer-storage": {"text": "memRequestBodyBytes defaults to 1048576 and controls the memory-to-disk threshold independently of maximum accepted body size.", "components": ["middleware"], "sources": ["middleware:sd987195d64dd"], "status": "REASONED"}, - "rate-limit": {"text": "average=10 and burst=20 configure a rate cap, per source by default.", "components": ["middleware"], "sources": ["middleware:scf1b1e35ef5d"], "status": "REASONED"}, - "inflight": {"text": "inflightreq.amount=10 bounds concurrent requests; sourceCriterion defaults to request host rather than client, so configure a per-client cap explicitly.", "components": ["middleware"], "sources": ["middleware:sd20dcba7ebca"], "status": "REASONED"}, - "middleware-order": {"text": "Rate/inflight precede auth to count rejected logins: Traefik v3.5.0 BasicAuth returns on failure without calling the next handler; buffering follows admission controls to avoid buffering rejected uploads.", "components": ["traefik-docs", "middleware", "traefik-basicauth-source"], "sources": ["traefik-docs:scbc0f2135c78", "middleware:sd987195d64dd", "middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca", "traefik-basicauth-source:s5f9a13d720c8"], "status": "REASONED"}, - "dashboard": {"text": "Keep api.insecure and unprotected api@internal routers off public entry points or protect the dashboard with auth.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED"}, - "swarm": {"text": "Standalone Docker guidance does not cover Swarm; v3 uses providers.swarm and its separate exposedByDefault also defaults true.", "components": ["swarm"], "sources": ["swarm:s256d46f3f558"], "status": "REASONED"}, - "verify-redirect": {"text": "HTTP should redirect to HTTPS.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, - "verify-auth": {"text": "Unauthenticated HTTPS should return 401 once auth is attached.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, - "verify-size": {"text": "Authenticated 1M must reach the app and 11M return 413; 401/000 invalidate the control. Remove app-body in isolation to rule out backend refusal.", "components": ["middleware", "traefik-docs"], "sources": ["middleware:sd987195d64dd", "traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, - "observed-429": {"text": "The guide records only a 429 response; no limiter attribution or exposed/fixed demonstration is established.", "components": ["middleware"], "sources": ["middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca"], "status": "DEMONSTRATED", "evidence": "A 429 appeared. That is all this shows."}, - "verify-rate": {"text": "Concurrent unauthenticated probes can reach pre-auth limiters; 429 alone cannot distinguish rate, inflight or upstream refusal. Compare each limiter alone against both disabled.", "components": ["middleware", "traefik-basicauth-source"], "sources": ["middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca", "traefik-basicauth-source:s5f9a13d720c8"], "status": "REASONED", "verify": [1]}, - "verify-isolation": {"text": "Compose ps covers one project; ss misses NAT-only publication without a userland proxy. Neither finds unintended routes through Traefik.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED", "verify": [1]}, - "verify-acme": {"text": "Inspect ACME startup errors; failed issuance can leave a self-signed TRAEFIK DEFAULT CERT.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, - "canary-route": {"text": "On a disposable deployment, unlabelled traefik/whoami has one HTTP port; use its generated Host rule and plain HTTP even on :443 because the generated router requests no TLS.", "components": ["docker", "traefik-docs"], "sources": ["docker:se23fe4fe79fa", "traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [2]}, - "verify-canary": {"text": "Calibrate exposedByDefault true with 200 and the canary Hostname body; false should give 404. Missing discovery/provider also gives 404, unreachable backend 502/504.", "components": ["docker", "curl"], "sources": ["docker:se23fe4fe79fa", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]}, - "verify-app-control": {"text": "In the same run, the app must return 401 or known authenticated 200 while the canary gives 404; app 401 proves routing, not proxy-auth attachment. Keep TLS verification.", "components": ["docker", "traefik-docs", "curl"], "sources": ["docker:se23fe4fe79fa", "traefik-docs:scbc0f2135c78", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [3]} - } -} ---- # Traefik: automatic TLS and authentication middleware - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-05; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- traefik-docs: Traefik documentation (rolling) (unknown) -- docker: Traefik Docker provider (unknown) -- swarm: Traefik Swarm provider (unknown) -- middleware: Traefik middleware documentation (unknown) -- curl: curl write-out minimum (7.75.0) -- traefik-v3: Traefik v3 documentation (v3.5) -- traefik-source: Traefik ACME storage source (v3.5.0) -- httpd-source: Apache httpd htpasswd source (2.4.69) -- owasp-source: OWASP Password Storage Cheat Sheet (72865b1e3b5754246a101778dc00b37f9c233373) -- cloudflare-source: Cloudflare Access MFA documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- traefik-basicauth-source: Traefik BasicAuth source (v3.5.0) - -Claims (full source mapping in the front matter): +Written against Traefik v3 documentation v3.5, Traefik ACME storage source v3.5.0, Traefik BasicAuth source v3.5.0, Apache httpd htpasswd source 2.4.69, and curl write-out minimum 7.75.0; 2 further sources are pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-10-05; documentation checked September 2026. The guide records 30 claims: 1 demonstrated by a check that ran and 29 reasoned from cited sources. -- entrypoints (REASONED; traefik-docs, traefik-v3): For Traefik v2/v3, web :80 redirects to HTTPS websecure :443 and an ACME resolver obtains/renews certificates. -- acme-storage (REASONED; traefik-docs, traefik-v3, traefik-source): Persist acme.json across restarts with mode 600 to retain certificates and avoid repeated issuance/rate limits. -- acme-challenges (REASONED; traefik-docs, traefik-v3): TLS-ALPN requires inbound 443; HTTP-01 uses 80, while DNS supports wildcards without inbound ports. -- docker-default (REASONED; docker): Docker discovery defaults exposedByDefault to true; set false and opt each intended container in with traefik.enable=true. -- docker-scope (REASONED; docker): Discovery is daemon-wide, not Compose-project scoped; eligible unlabelled containers get routers unless excluded. Missing usable ports skip service creation. -- docker-socket (REASONED; docker): The Docker socket gives daemon control despite :ro; use a restricted socket proxy to limit API access. -- tls-floor (REASONED; traefik-docs, traefik-v3): Dynamic default TLS options set minVersion VersionTLS12. -- route (REASONED; traefik-docs, docker): Docker labels select the app host, websecure, letsencrypt resolver and backend port 3000. -- backend-isolation (REASONED; docker): Publish only Traefik 80/443, not the app's 3000, to prevent middleware/TLS bypass. -- basic (REASONED; traefik-docs, traefik-v3, httpd-source, owasp-source): Attach bcrypt basicAuth to the router; Apache httpd 2.4.69 htpasswd -nB defaults to bcrypt cost 5, while -C 12 selects cost 12; OWASP at 72865b1e recommends 10 or more for legacy bcrypt. -- compose-hash (REASONED; traefik-docs, traefik-v3): Double each dollar sign in Compose hash labels; file-provider hashes need no such escaping. -- file-provider (REASONED; traefik-docs, traefik-v3): Load dynamic files with providers.file.directory/filename and reference their middleware from Docker as app-auth@file. -- mfa (REASONED; traefik-docs, traefik-v3, cloudflare-source): Basic is single-factor; use forwardAuth with Authelia/oauth2-proxy or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization). -- body-limit (REASONED; middleware): Attach buffering.maxRequestBodyBytes=10485760 alongside existing auth/routing labels; it bounds accepted body size. -- buffer-storage (REASONED; middleware): memRequestBodyBytes defaults to 1048576 and controls the memory-to-disk threshold independently of maximum accepted body size. -- rate-limit (REASONED; middleware): average=10 and burst=20 configure a rate cap, per source by default. -- inflight (REASONED; middleware): inflightreq.amount=10 bounds concurrent requests; sourceCriterion defaults to request host rather than client, so configure a per-client cap explicitly. -- middleware-order (REASONED; traefik-docs, middleware, traefik-basicauth-source): Rate/inflight precede auth to count rejected logins: Traefik v3.5.0 BasicAuth returns on failure without calling the next handler; buffering follows admission controls to avoid buffering rejected uploads. -- dashboard (REASONED; traefik-docs): Keep api.insecure and unprotected api@internal routers off public entry points or protect the dashboard with auth. -- swarm (REASONED; swarm): Standalone Docker guidance does not cover Swarm; v3 uses providers.swarm and its separate exposedByDefault also defaults true. -- verify-redirect (REASONED; traefik-docs): HTTP should redirect to HTTPS. -- verify-auth (REASONED; traefik-docs): Unauthenticated HTTPS should return 401 once auth is attached. -- verify-size (REASONED; middleware, traefik-docs): Authenticated 1M must reach the app and 11M return 413; 401/000 invalidate the control. Remove app-body in isolation to rule out backend refusal. -- observed-429 (DEMONSTRATED; middleware): The guide records only a 429 response; no limiter attribution or exposed/fixed demonstration is established. -- verify-rate (REASONED; middleware, traefik-basicauth-source): Concurrent unauthenticated probes can reach pre-auth limiters; 429 alone cannot distinguish rate, inflight or upstream refusal. Compare each limiter alone against both disabled. -- verify-isolation (REASONED; docker): Compose ps covers one project; ss misses NAT-only publication without a userland proxy. Neither finds unintended routes through Traefik. -- verify-acme (REASONED; traefik-docs): Inspect ACME startup errors; failed issuance can leave a self-signed TRAEFIK DEFAULT CERT. -- canary-route (REASONED; docker, traefik-docs): On a disposable deployment, unlabelled traefik/whoami has one HTTP port; use its generated Host rule and plain HTTP even on :443 because the generated router requests no TLS. -- verify-canary (REASONED; docker, curl): Calibrate exposedByDefault true with 200 and the canary Hostname body; false should give 404. Missing discovery/provider also gives 404, unreachable backend 502/504. -- verify-app-control (REASONED; docker, traefik-docs, curl): In the same run, the app must return 401 or known authenticated 200 while the canary gives 404; app 401 proves routing, not proxy-auth attachment. Keep TLS verification. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Applies to Traefik v2 and v3. Traefik obtains and renews certificates itself through ACME resolvers, which suits container deployments. @@ -433,3 +253,209 @@ With the authentication from section 3 in place, this unauthenticated probe retu - Apache httpd 2.4.69 htpasswd usage (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128 - OWASP Password Storage Cheat Sheet at 72865b1e3b5754246a101778dc00b37f9c233373, legacy bcrypt work factor (checked October 2026): https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13 - Cloudflare Access MFA documentation at c896795b8e8bab68d05ae78d64c9cbcda1fdb104, application or policy MFA (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/traefik.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- traefik-v3: Traefik v3 documentation (v3.5) +- traefik-source: Traefik ACME storage source (v3.5.0) +- traefik-basicauth-source: Traefik BasicAuth source (v3.5.0) +- traefik-docs: Traefik documentation (rolling) (unknown) +- docker: Traefik Docker provider (unknown) +- swarm: Traefik Swarm provider (unknown) +- middleware: Traefik middleware documentation (unknown) +- httpd-source: Apache httpd htpasswd source (2.4.69) +- cloudflare-source: Cloudflare Access MFA documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- curl: curl write-out minimum (7.75.0) +- owasp-source: OWASP Password Storage Cheat Sheet (72865b1e3b5754246a101778dc00b37f9c233373) + +Claims (full source mapping in the JSON block below): + +- entrypoints (REASONED; traefik-docs, traefik-v3): For Traefik v2/v3, web :80 redirects to HTTPS websecure :443 and an ACME resolver obtains/renews certificates. +- acme-storage (REASONED; traefik-docs, traefik-v3, traefik-source): Persist acme.json across restarts with mode 600 to retain certificates and avoid repeated issuance/rate limits. +- acme-challenges (REASONED; traefik-docs, traefik-v3): TLS-ALPN requires inbound 443; HTTP-01 uses 80, while DNS supports wildcards without inbound ports. +- docker-default (REASONED; docker): Docker discovery defaults exposedByDefault to true; set false and opt each intended container in with traefik.enable=true. +- docker-scope (REASONED; docker): Discovery is daemon-wide, not Compose-project scoped; eligible unlabelled containers get routers unless excluded. Missing usable ports skip service creation. +- docker-socket (REASONED; docker): The Docker socket gives daemon control despite :ro; use a restricted socket proxy to limit API access. +- tls-floor (REASONED; traefik-docs, traefik-v3): Dynamic default TLS options set minVersion VersionTLS12. +- route (REASONED; traefik-docs, docker): Docker labels select the app host, websecure, letsencrypt resolver and backend port 3000. +- backend-isolation (REASONED; docker): Publish only Traefik 80/443, not the app's 3000, to prevent middleware/TLS bypass. +- basic (REASONED; traefik-docs, traefik-v3, httpd-source, owasp-source): Attach bcrypt basicAuth to the router; Apache httpd 2.4.69 htpasswd -nB defaults to bcrypt cost 5, while -C 12 selects cost 12; OWASP at 72865b1e recommends 10 or more for legacy bcrypt. +- compose-hash (REASONED; traefik-docs, traefik-v3): Double each dollar sign in Compose hash labels; file-provider hashes need no such escaping. +- file-provider (REASONED; traefik-docs, traefik-v3): Load dynamic files with providers.file.directory/filename and reference their middleware from Docker as app-auth@file. +- mfa (REASONED; traefik-docs, traefik-v3, cloudflare-source): Basic is single-factor; use forwardAuth with Authelia/oauth2-proxy or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization). +- body-limit (REASONED; middleware): Attach buffering.maxRequestBodyBytes=10485760 alongside existing auth/routing labels; it bounds accepted body size. +- buffer-storage (REASONED; middleware): memRequestBodyBytes defaults to 1048576 and controls the memory-to-disk threshold independently of maximum accepted body size. +- rate-limit (REASONED; middleware): average=10 and burst=20 configure a rate cap, per source by default. +- inflight (REASONED; middleware): inflightreq.amount=10 bounds concurrent requests; sourceCriterion defaults to request host rather than client, so configure a per-client cap explicitly. +- middleware-order (REASONED; traefik-docs, middleware, traefik-basicauth-source): Rate/inflight precede auth to count rejected logins: Traefik v3.5.0 BasicAuth returns on failure without calling the next handler; buffering follows admission controls to avoid buffering rejected uploads. +- dashboard (REASONED; traefik-docs): Keep api.insecure and unprotected api@internal routers off public entry points or protect the dashboard with auth. +- swarm (REASONED; swarm): Standalone Docker guidance does not cover Swarm; v3 uses providers.swarm and its separate exposedByDefault also defaults true. +- verify-redirect (REASONED; traefik-docs): HTTP should redirect to HTTPS. +- verify-auth (REASONED; traefik-docs): Unauthenticated HTTPS should return 401 once auth is attached. +- verify-size (REASONED; middleware, traefik-docs): Authenticated 1M must reach the app and 11M return 413; 401/000 invalidate the control. Remove app-body in isolation to rule out backend refusal. +- observed-429 (DEMONSTRATED; middleware): The guide records only a 429 response; no limiter attribution or exposed/fixed demonstration is established. +- verify-rate (REASONED; middleware, traefik-basicauth-source): Concurrent unauthenticated probes can reach pre-auth limiters; 429 alone cannot distinguish rate, inflight or upstream refusal. Compare each limiter alone against both disabled. +- verify-isolation (REASONED; docker): Compose ps covers one project; ss misses NAT-only publication without a userland proxy. Neither finds unintended routes through Traefik. +- verify-acme (REASONED; traefik-docs): Inspect ACME startup errors; failed issuance can leave a self-signed TRAEFIK DEFAULT CERT. +- canary-route (REASONED; docker, traefik-docs): On a disposable deployment, unlabelled traefik/whoami has one HTTP port; use its generated Host rule and plain HTTP even on :443 because the generated router requests no TLS. +- verify-canary (REASONED; docker, curl): Calibrate exposedByDefault true with 200 and the canary Hostname body; false should give 404. Missing discovery/provider also gives 404, unreachable backend 502/504. +- verify-app-control (REASONED; docker, traefik-docs, curl): In the same run, the app must return 401 or known authenticated 200 while the canary gives 404; app 401 proves routing, not proxy-auth attachment. Keep TLS verification. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "aac661357effc89cf9b208294d0f255b4fb31c03428609d9756c4fa69347809c", + "components": { + "traefik-v3": { + "name": "Traefik v3 documentation", + "basis": "v3.5", + "sources": { + "sd6ad276040b7": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/entrypoints/", + "s418a6b1ad09b": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/tls/certificate-resolvers/acme/", + "s25d849b2d428": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/tls/tls-options/", + "s3acb3f5b1778": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/providers/others/file/", + "s5d7a6940f1dd": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/middlewares/basicauth/", + "s3cf7d994a56a": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/middlewares/forwardauth/" + } + }, + "traefik-source": { + "name": "Traefik ACME storage source", + "basis": "v3.5.0", + "sources": { + "s0e08b0bdcb35": "https://github.com/traefik/traefik/blob/v3.5.0/pkg/provider/acme/local_store.go#L125" + } + }, + "traefik-basicauth-source": { + "name": "Traefik BasicAuth source", + "basis": "v3.5.0", + "sources": { + "s5f9a13d720c8": "https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/basic_auth.go#L67-L99" + } + }, + "traefik-docs": { + "name": "Traefik documentation (rolling)", + "basis": "unknown", + "sources": { + "scbc0f2135c78": "https://doc.traefik.io/traefik/" + } + }, + "docker": { + "name": "Traefik Docker provider", + "basis": "unknown", + "sources": { + "se23fe4fe79fa": "https://doc.traefik.io/traefik/reference/install-configuration/providers/docker/" + } + }, + "swarm": { + "name": "Traefik Swarm provider", + "basis": "unknown", + "sources": { + "s256d46f3f558": "https://doc.traefik.io/traefik/reference/install-configuration/providers/swarm/" + } + }, + "middleware": { + "name": "Traefik middleware documentation", + "basis": "unknown", + "sources": { + "sd987195d64dd": "https://doc.traefik.io/traefik/middlewares/http/buffering/", + "sd20dcba7ebca": "https://doc.traefik.io/traefik/middlewares/http/inflightreq/", + "scf1b1e35ef5d": "https://doc.traefik.io/traefik/middlewares/http/ratelimit/" + } + }, + "httpd-source": { + "name": "Apache httpd htpasswd source", + "basis": "2.4.69", + "sources": { + "sf711558bdf3c": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L45", + "s4aa0f7f341fb": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L292-L295", + "s4482c55d79a2": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L382", + "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128" + } + }, + "cloudflare-source": { + "name": "Cloudflare Access MFA documentation", + "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" + } + }, + "curl": { + "name": "curl write-out minimum", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + }, + "owasp-source": { + "name": "OWASP Password Storage Cheat Sheet", + "basis": "72865b1e3b5754246a101778dc00b37f9c233373", + "sources": { + "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" + } + } + }, + "claims": { + "entrypoints": {"text": "For Traefik v2/v3, web :80 redirects to HTTPS websecure :443 and an ACME resolver obtains/renews certificates.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:sd6ad276040b7", "traefik-v3:s418a6b1ad09b"], "status": "REASONED"}, + "acme-storage": {"text": "Persist acme.json across restarts with mode 600 to retain certificates and avoid repeated issuance/rate limits.", "components": ["traefik-docs", "traefik-v3", "traefik-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s418a6b1ad09b", "traefik-source:s0e08b0bdcb35"], "status": "REASONED"}, + "acme-challenges": {"text": "TLS-ALPN requires inbound 443; HTTP-01 uses 80, while DNS supports wildcards without inbound ports.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s418a6b1ad09b"], "status": "REASONED"}, + "docker-default": {"text": "Docker discovery defaults exposedByDefault to true; set false and opt each intended container in with traefik.enable=true.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, + "docker-scope": {"text": "Discovery is daemon-wide, not Compose-project scoped; eligible unlabelled containers get routers unless excluded. Missing usable ports skip service creation.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, + "docker-socket": {"text": "The Docker socket gives daemon control despite :ro; use a restricted socket proxy to limit API access.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, + "tls-floor": {"text": "Dynamic default TLS options set minVersion VersionTLS12.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s25d849b2d428"], "status": "REASONED"}, + "route": {"text": "Docker labels select the app host, websecure, letsencrypt resolver and backend port 3000.", "components": ["traefik-docs", "docker"], "sources": ["traefik-docs:scbc0f2135c78", "docker:se23fe4fe79fa"], "status": "REASONED"}, + "backend-isolation": {"text": "Publish only Traefik 80/443, not the app's 3000, to prevent middleware/TLS bypass.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, + "basic": {"text": "Attach bcrypt basicAuth to the router; Apache httpd 2.4.69 htpasswd -nB defaults to bcrypt cost 5, while -C 12 selects cost 12; OWASP at 72865b1e recommends 10 or more for legacy bcrypt.", "components": ["traefik-docs", "traefik-v3", "httpd-source", "owasp-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s5d7a6940f1dd", "httpd-source:sf711558bdf3c", "httpd-source:s4aa0f7f341fb", "httpd-source:s4482c55d79a2", "httpd-source:s309ecdd34379", "owasp-source:s4531e65b6502"], "status": "REASONED"}, + "compose-hash": {"text": "Double each dollar sign in Compose hash labels; file-provider hashes need no such escaping.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s5d7a6940f1dd"], "status": "REASONED"}, + "file-provider": {"text": "Load dynamic files with providers.file.directory/filename and reference their middleware from Docker as app-auth@file.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s3acb3f5b1778"], "status": "REASONED"}, + "mfa": {"text": "Basic is single-factor; use forwardAuth with Authelia/oauth2-proxy or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization).", "components": ["traefik-docs", "traefik-v3", "cloudflare-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s3cf7d994a56a", "cloudflare-source:sdcac732541da"], "status": "REASONED"}, + "body-limit": {"text": "Attach buffering.maxRequestBodyBytes=10485760 alongside existing auth/routing labels; it bounds accepted body size.", "components": ["middleware"], "sources": ["middleware:sd987195d64dd"], "status": "REASONED"}, + "buffer-storage": {"text": "memRequestBodyBytes defaults to 1048576 and controls the memory-to-disk threshold independently of maximum accepted body size.", "components": ["middleware"], "sources": ["middleware:sd987195d64dd"], "status": "REASONED"}, + "rate-limit": {"text": "average=10 and burst=20 configure a rate cap, per source by default.", "components": ["middleware"], "sources": ["middleware:scf1b1e35ef5d"], "status": "REASONED"}, + "inflight": {"text": "inflightreq.amount=10 bounds concurrent requests; sourceCriterion defaults to request host rather than client, so configure a per-client cap explicitly.", "components": ["middleware"], "sources": ["middleware:sd20dcba7ebca"], "status": "REASONED"}, + "middleware-order": {"text": "Rate/inflight precede auth to count rejected logins: Traefik v3.5.0 BasicAuth returns on failure without calling the next handler; buffering follows admission controls to avoid buffering rejected uploads.", "components": ["traefik-docs", "middleware", "traefik-basicauth-source"], "sources": ["traefik-docs:scbc0f2135c78", "middleware:sd987195d64dd", "middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca", "traefik-basicauth-source:s5f9a13d720c8"], "status": "REASONED"}, + "dashboard": {"text": "Keep api.insecure and unprotected api@internal routers off public entry points or protect the dashboard with auth.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED"}, + "swarm": {"text": "Standalone Docker guidance does not cover Swarm; v3 uses providers.swarm and its separate exposedByDefault also defaults true.", "components": ["swarm"], "sources": ["swarm:s256d46f3f558"], "status": "REASONED"}, + "verify-redirect": {"text": "HTTP should redirect to HTTPS.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "Unauthenticated HTTPS should return 401 once auth is attached.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, + "verify-size": {"text": "Authenticated 1M must reach the app and 11M return 413; 401/000 invalidate the control. Remove app-body in isolation to rule out backend refusal.", "components": ["middleware", "traefik-docs"], "sources": ["middleware:sd987195d64dd", "traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, + "observed-429": {"text": "The guide records only a 429 response; no limiter attribution or exposed/fixed demonstration is established.", "components": ["middleware"], "sources": ["middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca"], "status": "DEMONSTRATED", "evidence": "A 429 appeared. That is all this shows."}, + "verify-rate": {"text": "Concurrent unauthenticated probes can reach pre-auth limiters; 429 alone cannot distinguish rate, inflight or upstream refusal. Compare each limiter alone against both disabled.", "components": ["middleware", "traefik-basicauth-source"], "sources": ["middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca", "traefik-basicauth-source:s5f9a13d720c8"], "status": "REASONED", "verify": [1]}, + "verify-isolation": {"text": "Compose ps covers one project; ss misses NAT-only publication without a userland proxy. Neither finds unintended routes through Traefik.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED", "verify": [1]}, + "verify-acme": {"text": "Inspect ACME startup errors; failed issuance can leave a self-signed TRAEFIK DEFAULT CERT.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, + "canary-route": {"text": "On a disposable deployment, unlabelled traefik/whoami has one HTTP port; use its generated Host rule and plain HTTP even on :443 because the generated router requests no TLS.", "components": ["docker", "traefik-docs"], "sources": ["docker:se23fe4fe79fa", "traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [2]}, + "verify-canary": {"text": "Calibrate exposedByDefault true with 200 and the canary Hostname body; false should give 404. Missing discovery/provider also gives 404, unreachable backend 502/504.", "components": ["docker", "curl"], "sources": ["docker:se23fe4fe79fa", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]}, + "verify-app-control": {"text": "In the same run, the app must return 401 or known authenticated 200 while the canary gives 404; app 401 proves routing, not proxy-auth attachment. Keep TLS verification.", "components": ["docker", "traefik-docs", "curl"], "sources": ["docker:se23fe4fe79fa", "traefik-docs:scbc0f2135c78", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [3]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/workflow-orchestrators.md b/plugin/skills/secure-deployment-config/references/workflow-orchestrators.md index 7d1af98fe..6d4c2be64 100644 --- a/plugin/skills/secure-deployment-config/references/workflow-orchestrators.md +++ b/plugin/skills/secure-deployment-config/references/workflow-orchestrators.md @@ -1,451 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-05", - "documentation_checked": "2026-10", - "body_sha256": "fa18bb5bca5189881826e754d7d45b7a5179143b73c35dd0f654f46b3e7945b1", - "components": { - "prefect": { - "name": "Prefect Basic Auth minimum", - "basis": "3.1.8", - "sources": { - "s12416d1a0d69": "https://github.com/PrefectHQ/prefect/releases/tag/3.1.8" - } - }, - "prefect-source": { - "name": "Prefect server source", - "basis": "9e560c9b6df4e19a5109a66e66d461f9facb538d", - "sources": { - "sa4574c7ed80f": "https://github.com/PrefectHQ/prefect/blob/9e560c9b6df4e19a5109a66e66d461f9facb538d/src/prefect/server/api/server.py" - } - }, - "dagster": { - "name": "Dagster OSS", - "basis": "1.13.24", - "sources": { - "sab1b42d92f27": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L41-L42", - "s6bc57f86dfe1": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L81-L96", - "s2455602a504b": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L306-L311", - "s3c7d8f17f245": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster/dagster/_cli/dev.py#L251-L252", - "s469c458c4a71": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L339-L351", - "s695dfd19dcca": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/templates/helpers/_deployment-webserver.tpl#L86-L90", - "sf6f027e96a0d": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/templates/helpers/_helpers.tpl#L55", - "seda3a1b24425": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/values.yaml#L54-L58" - } - }, - "airflow": { - "name": "Apache Airflow", - "basis": "3.3.2", - "sources": { - "s1d0b86fe4334": "https://airflow.apache.org/docs/apache-airflow/3.3.2/security/security_model.html", - "sad02a49561a0": "https://airflow.apache.org/docs/apache-airflow/3.3.2/core-concepts/auth-manager/simple/index.html", - "s3be3e4616150": "https://airflow.apache.org/docs/apache-airflow/3.3.2/core-concepts/auth-manager/index.html", - "s78c661193a1a": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#simple-auth-manager-all-admins", - "s963b49f58a13": "https://airflow.apache.org/docs/apache-airflow/3.3.2/security/api.html", - "s705f1eb5de00": "https://raw.githubusercontent.com/apache/airflow/3.3.2/airflow-core/docs/howto/docker-compose/docker-compose.yaml", - "sdfcfd543928c": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#secret-key", - "s4ed64009ed8f": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#expose-config" - } - }, - "airflow-two": { - "name": "Apache Airflow historical configuration", - "basis": "2.11.0", - "sources": { - "sb97f3044e215": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#auth-backends", - "sabb71378ad74": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#secret-key", - "s6813801b7880": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#expose-config" - } - }, - "airflow-old": { - "name": "Apache Airflow historical default", - "basis": "2.2.5", - "sources": { - "s4e72b1f51038": "https://airflow.apache.org/docs/apache-airflow/2.2.5/configurations-ref.html#auth-backend" - } - }, - "flower": { - "name": "Flower", - "basis": "v2.2.0", - "sources": { - "s25760fae7152": "https://github.com/mher/flower/blob/v2.2.0/flower/options.py#L7-L15", - "s1f72a8415337": "https://github.com/mher/flower/blob/v2.2.0/flower/options.py#L56-L57", - "s4dd518442b7f": "https://github.com/mher/flower/blob/v2.2.0/flower/command.py#L39-L91", - "s4e49ebc71dc1": "https://github.com/mher/flower/blob/v2.2.0/flower/app.py#L71-L76", - "s0fe6c77e9c9d": "https://github.com/mher/flower/blob/v2.2.0/flower/app.py#L85-L89" - } - }, - "tornado": { - "name": "Tornado", - "basis": "v6.5.10", - "sources": { - "sc503b4425831": "https://github.com/tornadoweb/tornado/blob/v6.5.10/tornado/netutil.py#L72-L73", - "s737c51cccd00": "https://github.com/tornadoweb/tornado/blob/v6.5.10/tornado/netutil.py#L215-L228" - } - }, - "linux": { - "name": "Linux permissions and tools", - "basis": "unknown", - "sources": { - "s8ffd93b0efa4": "https://man7.org/linux/man-pages/man7/unix.7.html", - "sfb53d2819cd0": "https://man7.org/linux/man-pages/man7/path_resolution.7.html", - "s31f41c54750c": "https://man7.org/linux/man-pages/man5/acl.5.html", - "s3cf3d5be0f64": "https://man7.org/linux/man-pages/man7/user_namespaces.7.html", - "sa5ccbfea6e16": "https://www.kernel.org/doc/html/latest/filesystems/idmappings.html", - "s08902025e5f9": "https://man7.org/linux/man-pages/man1/getfacl.1.html", - "sa0d3793fabb7": "https://man7.org/linux/man-pages/man1/stat.1.html" - } - }, - "systemd": { - "name": "systemd", - "basis": "v257", - "sources": { - "s48ece26c16d3": "https://github.com/systemd/systemd/blob/v257/man/systemd.exec.xml#L1630-L1640" - } - }, - "fhs": { - "name": "Filesystem Hierarchy Standard", - "basis": "3.0", - "sources": { - "seafd8219f367": "https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch03s15.html" - } - }, - "docker": { - "name": "Moby Docker", - "basis": "docker-v29.8.1", - "sources": { - "s1f268e247a1f": "https://github.com/moby/moby/blob/docker-v29.8.1/daemon/pkg/oci/caps/defaults.go#L4-L7" - } - }, - "argo-three": { - "name": "Argo Workflows", - "basis": "v3.7.18", - "sources": { - "s262098553d2f": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/cmd/argo/commands/server.go#L191-L229", - "sbddb9f07a301": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/apiserver/argoserver.go#L263-L282", - "s2ca4f14c3164": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/gatekeeper.go#L166-L218", - "sfbb904619bfa": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/overlays/argo-server-deployment.yaml#L1-L15", - "s0ac72aa1a86c": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterole.yaml#L1-L65", - "s1689f25010be": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/cluster-workflow-template-rbac.yaml#L1-L58", - "sf0c3a52d3302": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/Makefile#L493-L515", - "sfdcdf614ee37": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/base/argo-server/argo-server-deployment.yaml#L14-L34", - "sc7ca3a37b16c": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/overlays/argo-server-deployment.yaml#L1-L4", - "s01f102470d97": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/base/argo-server/argo-server-service.yaml#L1-L11", - "s49cdaf0691c0": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/mode.go#L33-L44", - "sdb4a0c18b7c6": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/cmd/argo/commands/server.go#L113-L137", - "s36759aec55fa": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/apiserver/argoserver.go#L430-L449", - "s862033e52c9b": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-minimal.yaml#L3613-L3664", - "s73e9beeef775": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-mysql.yaml#L3678-L3729", - "s476ff15db454": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-postgres.yaml#L3677-L3728", - "sa4b2fa875915": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterolebinding.yaml#L1-L11", - "s48db5ba07d23": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/kustomization.yaml#L4-L5", - "se3ffdf2973a6": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/argo-server-rbac/argo-server-role.yaml#L1-L65", - "sb901e7f87b54": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/argo-server-rbac/argo-server-rolebinding.yaml#L1-L11", - "s49a8855b6da2": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/docs/argo-server-auth-mode.md#L3-L9", - "s64e74b0dbe95": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/gatekeeper.go#L106-L121" - } - }, - "argo-four": { - "name": "Argo Workflows", - "basis": "v4.1.4", - "sources": { - "s774215022573": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/cmd/argo/commands/server.go#L195-L239", - "s0f074a9ac21e": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/apiserver/argoserver.go#L299-L320", - "sb284f93cde9c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/gatekeeper.go#L189-L244", - "s0871a1dd9b13": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/docs/argo-server-auth-mode.md#L3-L9", - "sa7b9db8384be": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start/base/overlays/argo-server-deployment.yaml#L1-L15", - "s79648b46d3f5": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-telemetry.yaml#L192919-L192970", - "sd2706d04971d": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterole.yaml#L1-L67", - "sd920da4ad4b7": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/docs/quick-start.md#L3-L34", - "s144b824252c1": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/Makefile#L608-L634", - "s51645bfbc47c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/base/argo-server/argo-server-deployment.yaml#L14-L34", - "s5188f6f2efbb": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/overlays/argo-server-deployment.yaml#L1-L4", - "s639e9fe0024e": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/base/argo-server/argo-server-service.yaml#L1-L11", - "s4613c15c72af": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/mode.go#L33-L44", - "s0b16f1ac50df": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/cmd/argo/commands/server.go#L119-L143", - "sa120f74372eb": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/apiserver/argoserver.go#L476-L494", - "s6c08a5f34f2c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-minimal.yaml#L192772-L192823", - "sa0bf6187d1db": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-mysql.yaml#L192838-L192889", - "s31f4a149576b": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-postgres.yaml#L192837-L192888", - "s3be9b03e29be": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterolebinding.yaml#L1-L11", - "sc263bd937cf0": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start/base/kustomization.yaml#L4-L5", - "s1b134f96428f": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/argo-server-rbac/argo-server-role.yaml#L1-L67", - "s2418ee1aa49d": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/argo-server-rbac/argo-server-rolebinding.yaml#L1-L11", - "se758d83aaf1f": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/gatekeeper.go#L133-L148" - } - }, - "argo-docs": { - "name": "Argo Workflows documentation", - "basis": "release-3.7", - "sources": { - "s6c8e78288632": "https://argo-workflows.readthedocs.io/en/release-3.7/security/", - "s9898d4653a47": "https://argo-workflows.readthedocs.io/en/release-3.7/argo-server-sso/", - "s96145eacf345": "https://argo-workflows.readthedocs.io/en/release-3.7/rest-examples/" - } - }, - "iproute2": { - "name": "iproute2 ss manual", - "basis": "v6.15.0", - "sources": { - "s6cc7f285a0c7": "https://github.com/iproute2/iproute2/blob/v6.15.0/man/man8/ss.8#L358-L359", - "s9fa9a260f1de": "https://github.com/iproute2/iproute2/blob/v6.15.0/man/man8/ss.8#L7-L12" - } - }, - "network-namespaces": { - "name": "Linux network namespaces manual", - "basis": "man-pages-5.13", - "sources": { - "s427c923e346f": "https://github.com/mkerrisk/man-pages/blob/man-pages-5.13/man7/network_namespaces.7#L30-L40" - } - }, - "prefect-rolling": { - "name": "Prefect documentation (rolling)", - "basis": "unknown", - "sources": { - "sbe64d51110a2": "https://docs.prefect.io/v3/how-to-guides/self-hosted/server-cli", - "s0ef73a227667": "https://docs.prefect.io/v3/advanced/security-settings" - } - }, - "dagster-rolling": { - "name": "Dagster documentation (rolling)", - "basis": "unknown", - "sources": { - "s4c04d5da5257": "https://docs.dagster.io/guides/operate/webserver" - } - }, - "airflow-rolling": { - "name": "Apache Airflow documentation (rolling)", - "basis": "unknown", - "sources": { - "s2d1891641c91": "https://airflow.apache.org/docs/apache-airflow/stable/security/", - "s39bfd91c507b": "https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html" - } - }, - "temporal-rolling": { - "name": "Temporal documentation (rolling)", - "basis": "unknown", - "sources": { - "s12f2c64ad396": "https://docs.temporal.io/cli/command-reference/server", - "s06d4f0dbd0c8": "https://docs.temporal.io/self-hosted-guide/security", - "sa9d5614385ca": "https://docs.temporal.io/references/web-ui-configuration" - } - }, - "flower-rolling": { - "name": "Flower documentation (rolling)", - "basis": "unknown", - "sources": { - "sb1ba69f79c9b": "https://flower.readthedocs.io/en/latest/config.html" - } - }, - "prefect-blocks": { - "name": "Prefect Blocks source", - "basis": "3.1.8", - "sources": { - "s56081101d16a": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/server/api/block_documents.py#L47-L70", - "s1eae2c5f9f5e": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/server/api/block_documents.py#L101-L119", - "s46e69a2090c4": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/blocks/system.py#L154-L159" - } - }, - "fab-source": { - "name": "Apache Airflow FAB provider source", - "basis": "providers-fab/3.9.0", - "sources": { - "s3c94f0f7176a": "https://github.com/apache/airflow/blob/providers-fab/3.9.0/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py#L327-L381", - "s5552a28ab9d8": "https://github.com/apache/airflow/blob/providers-fab/3.9.0/providers/fab/pyproject.toml#L75-L81" - } - }, - "airflow-crypto": { - "name": "Apache Airflow crypto source", - "basis": "3.3.2", - "sources": { - "sdeaf2f54c054": "https://github.com/apache/airflow/blob/3.3.2/airflow-core/src/airflow/models/crypto.py#L47-L68", - "s70ea7631e820": "https://github.com/apache/airflow/blob/3.3.2/airflow-core/src/airflow/models/crypto.py#L154-L173" - } - }, - "temporal-codec": { - "name": "Temporal Go SDK codec source", - "basis": "v1.28.0", - "sources": { - "s24435f531da0": "https://github.com/temporalio/sdk-go/blob/v1.28.0/converter/codec.go#L284-L350" - } - }, - "fab-rolling": { - "name": "Apache Airflow FAB provider documentation (rolling)", - "basis": "unknown", - "sources": { - "s989f09d05161": "https://airflow.apache.org/docs/apache-airflow-providers-fab/stable/auth-manager/api-authentication.html" - } - } - }, - "claims": { - "fronting": {"text": "Keep UI/API origins private behind HTTPS and authentication; use IdP MFA for browsers and separate machine authentication, and constrain worker egress.", "components": ["airflow", "argo-docs", "temporal-rolling"], "sources": ["airflow:s1d0b86fe4334", "temporal-rolling:s06d4f0dbd0c8", "argo-docs:s6c8e78288632"], "status": "REASONED"}, - "prefect-auth": {"text": "Self-hosted Prefect defaults unauthenticated; Basic Auth from 3.1.8 uses matching server/client auth strings and prompts the UI.", "components": ["prefect", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, - "prefect-key": {"text": "Cloud-only PREFECT_API_KEY takes precedence over PREFECT_API_AUTH_STRING and produces 401 against a self-hosted server.", "components": ["prefect", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, - "prefect-secrets": {"text": "Protect the auth string and credential-bearing Blocks; Prefect 3.1.8 block-document read routes accept include_secrets, and Secret.get() returns the underlying value. Restrict API and worker access; UI masking does not enforce authorization.", "components": ["prefect-blocks", "prefect-rolling"], "sources": ["prefect-blocks:s56081101d16a", "prefect-blocks:s1eae2c5f9f5e", "prefect-blocks:s46e69a2090c4", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, - "prefect-port": {"text": "The self-hosted Prefect server uses port 4200.", "components": ["prefect-rolling"], "sources": ["prefect-rolling:sbe64d51110a2"], "status": "REASONED"}, - "dagster-bind": {"text": "Direct dagster-webserver/dev defaults to 127.0.0.1:3000, with flag/environment overrides and free-port fallback when the port is unset or 0; explicitly bind the private origin.", "components": ["dagster"], "sources": ["dagster:sab1b42d92f27", "dagster:s6bc57f86dfe1", "dagster:s2455602a504b", "dagster:s3c7d8f17f245", "dagster:s469c458c4a71"], "status": "REASONED"}, - "dagster-helm": {"text": "The chart passes -h 0.0.0.0 on the Service port, default 80, with ClusterIP Service by default.", "components": ["dagster"], "sources": ["dagster:s695dfd19dcca", "dagster:sf6f027e96a0d", "dagster:seda3a1b24425"], "status": "REASONED"}, - "dagster-auth": {"text": "OSS webserver has no built-in login or access control; protect all routes with an authenticating proxy.", "components": ["dagster-rolling"], "sources": ["dagster-rolling:s4c04d5da5257"], "status": "REASONED"}, - "airflow-boundary": {"text": "Airflow assumes authenticated known users and is not designed for untrusted public exposure; deployment managers must keep it private.", "components": ["airflow"], "sources": ["airflow:s1d0b86fe4334"], "status": "REASONED"}, - "airflow-two-api": {"text": "Airflow 2.11.0 uses [api] auth_backends with session default; before 2.3 the name was auth_backend and 2.2.5 defaulted to deny_all.", "components": ["airflow-two", "airflow-old"], "sources": ["airflow-two:sb97f3044e215", "airflow-old:s4e72b1f51038"], "status": "REASONED"}, - "airflow-simple": {"text": "Airflow 3 defaults to development-only Simple Auth Manager, with configured users/roles and generated passwords printed to logs unless supplied.", "components": ["airflow"], "sources": ["airflow:sad02a49561a0", "airflow:s3be3e4616150"], "status": "REASONED"}, - "airflow-fab": {"text": "For production install FAB and select FabAuthManager through [core] auth_manager; verify the effective manager and use an identity backend with MFA.", "components": ["airflow", "fab-rolling"], "sources": ["airflow:s3be3e4616150", "fab-rolling:s989f09d05161"], "status": "REASONED"}, - "airflow-api": {"text": "Airflow 3 public API uses JWT independently of [fab] auth_backends, which selects FAB API backends rather than the auth manager.", "components": ["airflow", "fab-rolling"], "sources": ["airflow:s3be3e4616150", "fab-rolling:s989f09d05161", "airflow:s963b49f58a13"], "status": "REASONED"}, - "airflow-all-admins": {"text": "Keep simple_auth_manager_all_admins unset or False; enabling it disables login and grants every visitor admin.", "components": ["airflow"], "sources": ["airflow:sad02a49561a0", "airflow:s78c661193a1a"], "status": "REASONED"}, - "fab-public-role": {"text": "Leave [fab] auth_role_public and legacy AUTH_ROLE_PUBLIC unset; a configured public role can make FAB treat an anonymous visitor as logged in and supply that role's permissions.", "components": ["fab-source"], "sources": ["fab-source:s3c94f0f7176a", "fab-source:s5552a28ab9d8"], "status": "REASONED"}, - "airflow-compose-account": {"text": "Development Compose selects FAB but defaults to airflow/airflow; set credentials before account creation and update/delete an existing account separately.", "components": ["airflow-rolling", "fab-rolling", "airflow"], "sources": ["airflow-rolling:s39bfd91c507b", "fab-rolling:s989f09d05161", "airflow:s705f1eb5de00"], "status": "REASONED"}, - "airflow-jwt": {"text": "Replace Compose fallback AIRFLOW__API_AUTH__JWT_SECRET=airflow_jwt_secret and share the strong value with all signers/validators; the public fallback permits token forgery.", "components": ["airflow-rolling", "airflow"], "sources": ["airflow-rolling:s39bfd91c507b", "airflow:s705f1eb5de00"], "status": "REASONED"}, - "airflow-secret-key": {"text": "Provision the independent secret_key too: [webserver] on 2.11.0 and [api] on 3.3.2.", "components": ["airflow-two", "airflow-rolling", "airflow"], "sources": ["airflow-rolling:s2d1891641c91", "airflow-two:sabb71378ad74", "airflow:sdfcfd543928c"], "status": "REASONED"}, - "airflow-config": {"text": "Keep configuration exposure off with WEBSERVER__EXPOSE_CONFIG on 2.11.0 or API__EXPOSE_CONFIG on 3.3.2.", "components": ["airflow-two", "airflow-rolling", "airflow"], "sources": ["airflow-rolling:s2d1891641c91", "airflow-two:s6813801b7880", "airflow:s4ed64009ed8f"], "status": "REASONED"}, - "airflow-fernet": {"text": "Protect Connections, Variables, database and Fernet keys; an empty [core] FERNET_KEY selects a no-encryption implementation for new values, while configured comma-separated keys use MultiFernet. Preserve keys needed to decrypt existing values; authorized workloads still use secrets.", "components": ["airflow-crypto", "airflow-rolling"], "sources": ["airflow-crypto:sdeaf2f54c054", "airflow-crypto:s70ea7631e820", "airflow-rolling:s2d1891641c91"], "status": "REASONED"}, - "airflow-port": {"text": "Compose publishes 8080 on all interfaces; use 127.0.0.1:8080:8080 behind the fronting layer.", "components": ["airflow-rolling"], "sources": ["airflow-rolling:s39bfd91c507b"], "status": "REASONED"}, - "temporal-authorizer": {"text": "Empty authorizer selects noopAuthorizer allowing every API request, including administration; configure default authorization with trusted JWT keys and audience.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, - "temporal-claims": {"text": "Empty claimMapper selects a no-op granting system-admin claims; configure both Authorizer and ClaimMapper, with frontend TLS.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, - "temporal-health": {"text": "The built-in default authorizer permits health checks without claims; health is not an authorization discriminator.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, - "temporal-ui": {"text": "UI OIDC auth.enabled is a sibling of providers; TEMPORAL_AUTH_ENABLED plus provider settings gates the UI only, with MFA at the IdP.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:sa9d5614385ca"], "status": "REASONED"}, - "temporal-tls": {"text": "Internode and frontend mTLS are separate from UI login and API authorization.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, - "temporal-payloads": {"text": "Keep plaintext credentials out of persisted inputs/results/history; the Temporal Go SDK v1.28.0 codec HTTP handler accepts encode/decode POSTs without an authentication check, so authenticate and restrict a deployed Codec Server separately.", "components": ["temporal-rolling", "temporal-codec"], "sources": ["temporal-rolling:s06d4f0dbd0c8", "temporal-codec:s24435f531da0"], "status": "REASONED"}, - "temporal-ports": {"text": "Frontend gRPC uses 7233 and the Web UI 8233 in the cited CLI server reference.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s12f2c64ad396"], "status": "REASONED"}, - "flower-bind": {"text": "Flower defaults to wildcard address and 5555 unless Unix socket, FLOWER_ variables or implicit working-directory flowerconfig.py override; bind loopback explicitly.", "components": ["flower", "tornado"], "sources": ["flower:s25760fae7152", "flower:s1f72a8415337", "flower:s4dd518442b7f", "flower:s4e49ebc71dc1", "tornado:sc503b4425831"], "status": "REASONED"}, - "flower-api": {"text": "Authentication defaults off; unauthenticated HTTP API is disabled unless FLOWER_UNAUTHENTICATED_API=true, which must not be mistaken for dashboard protection.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, - "flower-basic": {"text": "Basic auth uses a comma-separated credential list; supply it through FLOWER_BASIC_AUTH or protected config rather than argv and retain private TLS fronting.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, - "flower-oauth": {"text": "OAuth needs handler, client key/secret, redirect and allowed-email regex; protect the secret outside argv and prefer an MFA-enforcing provider.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, - "flower-socket": {"text": "Flower passes fixed mode 0777; Tornado removes/recreates filesystem sockets on startup, so chmod does not persist. Abstract sockets have no filesystem permission boundary.", "components": ["flower", "tornado", "linux"], "sources": ["flower:s0fe6c77e9c9d", "tornado:s737c51cccd00", "linux:s8ffd93b0efa4"], "status": "REASONED"}, - "flower-directory": {"text": "Restrict socket directory search with Flower ownership, proxy-only group, mode 0750 and no outsider ACL; directory access is essential because socket mode permits writes.", "components": ["linux"], "sources": ["linux:sfb53d2819cd0", "linux:s8ffd93b0efa4", "linux:s31f41c54750c"], "status": "REASONED"}, - "flower-capabilities": {"text": "Applicable DAC capabilities bypass directory search checks; Docker defaults include CAP_DAC_OVERRIDE, while user namespaces and idmapped mounts change identity interpretation.", "components": ["linux", "docker"], "sources": ["linux:s3cf3d5be0f64", "linux:sa5ccbfea6e16", "docker:s1f268e247a1f"], "status": "REASONED"}, - "flower-runtime-dir": {"text": "Reapply directory owner/group/mode on creation: /run is cleared at boot and RuntimeDirectory defaults 0755 unless RuntimeDirectoryMode overrides it.", "components": ["systemd", "fhs"], "sources": ["systemd:s48ece26c16d3", "fhs:seafd8219f367"], "status": "REASONED"}, - "flower-numeric-ids": {"text": "A proxy sharing the socket directory must hold the required numeric group after namespace/mount mappings; names alone do not establish access.", "components": ["linux"], "sources": ["linux:s3cf3d5be0f64", "linux:sa5ccbfea6e16"], "status": "REASONED"}, - "argo-bind": {"text": "Argo Server binds wildcard :2746 with host-dependent IPv4/IPv6 support and no bind-address flag.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s262098553d2f", "argo-four:s774215022573", "argo-three:sbddb9f07a301", "argo-four:s0f074a9ac21e"], "status": "REASONED"}, - "argo-defaults": {"text": "Both tags default secure=true, auth-mode=client and hsts=true; flags and ARGO_ environment equivalents can override.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s262098553d2f", "argo-four:s774215022573"], "status": "REASONED"}, - "argo-client": {"text": "Client mode uses caller Kubernetes credentials/RBAC; v4.1.4 validates via SelfSubjectReview and requires Kubernetes 1.28+.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c", "argo-four:s0871a1dd9b13"], "status": "REASONED"}, - "argo-quickstart": {"text": "Both tags' minimal/mysql/postgres quick-starts enable server plus client mode while retaining TLS; v4.1.4 telemetry does too. Anonymous callers inherit server permissions.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sfbb904619bfa", "argo-four:sa7b9db8384be", "argo-four:s79648b46d3f5", "argo-three:s862033e52c9b", "argo-three:s73e9beeef775", "argo-three:s476ff15db454", "argo-three:s262098553d2f", "argo-three:s49cdaf0691c0", "argo-three:s2ca4f14c3164", "argo-four:s6c08a5f34f2c", "argo-four:sa0bf6187d1db", "argo-four:s31f4a149576b", "argo-four:s774215022573", "argo-four:s4613c15c72af", "argo-four:sb284f93cde9c"], "status": "REASONED"}, - "argo-quickstart-rbac": {"text": "Quick-start binds cluster-wide workflow/template and Secret get/create permissions, plus cluster-workflow-template rights; it is demo-only.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s0ac72aa1a86c", "argo-four:sd2706d04971d", "argo-three:s1689f25010be", "argo-four:sd920da4ad4b7", "argo-three:sa4b2fa875915", "argo-three:s48db5ba07d23", "argo-four:s3be9b03e29be", "argo-four:sc263bd937cf0"], "status": "REASONED"}, - "argo-installs": {"text": "Traced install inputs keep client auth/TLS with cluster-wide server RBAC; namespace-install adds --namespaced and namespace RBAC. Generated install YAML was absent.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sf0c3a52d3302", "argo-four:s144b824252c1", "argo-three:sfdcdf614ee37", "argo-four:s51645bfbc47c", "argo-three:sc7ca3a37b16c", "argo-four:s5188f6f2efbb", "argo-three:s262098553d2f", "argo-three:s0ac72aa1a86c", "argo-three:sa4b2fa875915", "argo-three:se3ffdf2973a6", "argo-three:sb901e7f87b54", "argo-four:s774215022573", "argo-four:sd2706d04971d", "argo-four:s3be9b03e29be", "argo-four:s1b134f96428f", "argo-four:s2418ee1aa49d"], "status": "REASONED"}, - "argo-service": {"text": "Base Service omits type, defaulting to ClusterIP, and maps 2746 to 2746; this does not authenticate reachable callers.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s01f102470d97", "argo-four:s639e9fe0024e"], "status": "REASONED"}, - "argo-fallback": {"text": "Remove server auth mode: adding client/SSO does not prevent anonymous fallback to server credentials; local mode uses the server kubeconfig.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s49cdaf0691c0", "argo-four:s4613c15c72af", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c", "argo-four:s0871a1dd9b13", "argo-three:s49a8855b6da2"], "status": "REASONED"}, - "argo-sso": {"text": "Enable sso.rbac.enabled and map groups to narrow ServiceAccounts through rbac-rule/precedence; without SSO RBAC, authenticated users share server permissions.", "components": ["argo-docs", "argo-three", "argo-four"], "sources": ["argo-docs:s9898d4653a47", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c"], "status": "REASONED"}, - "argo-secrets": {"text": "Store SSO OAuth credentials in referenced Kubernetes Secrets and supply API tokens through protected input, not argv.", "components": ["argo-docs"], "sources": ["argo-docs:s9898d4653a47", "argo-docs:s96145eacf345"], "status": "REASONED"}, - "argo-tls": {"text": "Keep TLS enabled with a trusted certificate Secret; no name generates a self-signed certificate, failed named-Secret loading fails startup, secure=false is plaintext and defeats HSTS.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sdb4a0c18b7c6", "argo-four:s0b16f1ac50df", "argo-three:s36759aec55fa", "argo-four:sa120f74372eb"], "status": "REASONED"}, - "argo-metrics": {"text": "/metrics shares the server listener and auth gate unless ARGO_SERVER_METRICS_AUTH=false; server mode also admits anonymous metrics.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s36759aec55fa", "argo-four:sa120f74372eb", "argo-three:s49cdaf0691c0", "argo-three:s2ca4f14c3164", "argo-four:s4613c15c72af", "argo-four:sb284f93cde9c", "argo-three:s64e74b0dbe95", "argo-four:se758d83aaf1f"], "status": "REASONED"}, - "argo-network": {"text": "Keep 2746 private behind authenticated HTTPS; restrict direct Pod/Service access and scope server, SSO and execution-account permissions. Workflow submission permits arbitrary containers unless constrained.", "components": ["argo-docs"], "sources": ["argo-docs:s6c8e78288632"], "status": "REASONED"}, - "verify-inventory": {"text": "ss inventories only the current namespace, not publication, routing or authentication; inspect publications and external reachability separately.", "components": ["flower", "argo-three", "iproute2", "network-namespaces", "prefect-rolling"], "sources": ["prefect-rolling:sbe64d51110a2", "flower:s25760fae7152", "argo-three:sbddb9f07a301", "iproute2:s6cc7f285a0c7", "network-namespaces:s427c923e346f", "iproute2:s9fa9a260f1de"], "status": "REASONED", "verify": [1]}, - "verify-prefect": {"text": "Anonymous POST /api/flows/filter returning a JSON list is exposed; fixed is 401 with an authorized list on the same origin. Health/ready GET exemptions are version-dependent.", "components": ["prefect", "prefect-source", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-source:sa4574c7ed80f", "prefect-rolling:s0ef73a227667"], "status": "REASONED", "verify": [1]}, - "verify-dagster": {"text": "A RepositoryConnection from /graphql is a read, including empty nodes; GraphQL/transport errors are inconclusive. Test proxy authentication and origin isolation separately.", "components": ["dagster-rolling"], "sources": ["dagster-rolling:s4c04d5da5257"], "status": "REASONED", "verify": [1]}, - "verify-airflow": {"text": "FAB 3.9.0 returning 201 with access_token for airflow/airflow is exposed; pair a valid account and anonymous/authenticated API reads at proxy and origin. Airflow 2 uses its own API/auth.", "components": ["airflow-two", "airflow-rolling", "fab-rolling"], "sources": ["fab-rolling:s989f09d05161", "airflow-rolling:s39bfd91c507b", "airflow-two:sb97f3044e215"], "status": "REASONED", "verify": [1]}, - "verify-temporal": {"text": "Credential-free workflow listing on 7233 is exposed; require rejection and a matched authorized call, not health, TLS errors or missing namespaces as proof of auth.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8", "temporal-rolling:s12f2c64ad396"], "status": "REASONED", "verify": [1]}, - "verify-flower": {"text": "Anonymous UI/API should yield Basic 401 or OAuth login redirect, with a valid-session control; API-disabled is not proof the dashboard is protected.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED", "verify": [1]}, - "verify-socket": {"text": "Directory/socket permissions, outsider denial, reboot persistence and container numeric IDs remain reasoned; no socket or Flower process ran.", "components": ["flower", "linux"], "sources": ["flower:s0fe6c77e9c9d", "linux:s31f41c54750c", "linux:s08902025e5f9", "linux:sa0d3793fabb7"], "status": "REASONED", "verify": [1]}, - "flower-stat-run": {"text": "Tmpfs directory-only stat distinguished 0755 and 0750; the 0750 directory initially had only base ACL entries.", "components": ["linux"], "sources": ["linux:sa0d3793fabb7", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "`stat -c '%a %U:%G %u:%g %n'` printed `755` for a directory created with mode `0755` and `750` for one created with `0750`; `getfacl -p` on the `0750` directory listed only `user::rwx`, `group::r-x` and `other::---`."}, - "flower-acl-run": {"text": "A named nobody search ACL remained invisible in stat mode 750; ls and getfacl exposed the entry.", "components": ["linux"], "sources": ["linux:s31f41c54750c", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "After `setfacl -m u:nobody:--x` on the `0750` directory, `stat` still printed `750`. Only the trailing `+` in `ls -ld` and getfacl's `user:nobody:--x` line showed the named entry."}, - "flower-mask-run": {"text": "Changing the ACL mask hid named search permission; chmod 0750 restored its reported effectiveness. Access as nobody was not tested.", "components": ["linux"], "sources": ["linux:s31f41c54750c", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "After `setfacl -m m::r--`, getfacl printed `user:nobody:--x` with `#effective:---`. While masked, `stat` printed `740`."}, - "verify-argo": {"text": "Anonymous workflow JSON is exposed when server RBAC permits; missing tokens should yield 401 without server mode. A 403 alone is inconclusive; pair authorized 200 and inspect auth modes.", "components": ["argo-three", "argo-four", "argo-docs"], "sources": ["argo-three:s49cdaf0691c0", "argo-four:s4613c15c72af", "argo-docs:s96145eacf345", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c"], "status": "REASONED", "verify": [1]}, - "verify-origin": {"text": "Any HTTP response from the origin at an untrusted vantage proves publication; use the same responding allowed origin as control and treat connection errors/timeouts as inconclusive.", "components": ["airflow", "argo-docs"], "sources": ["airflow:s1d0b86fe4334", "argo-docs:s6c8e78288632"], "status": "REASONED", "verify": [1]} - } -} ---- -# Workflow and agent orchestrators: Prefect, Dagster, Airflow, Temporal, Flower, Argo - - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-05; documentation checked 2026-10 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- prefect: Prefect Basic Auth minimum (3.1.8) -- prefect-source: Prefect server source (9e560c9b6df4e19a5109a66e66d461f9facb538d) -- dagster: Dagster OSS (1.13.24) -- airflow: Apache Airflow (3.3.2) -- airflow-two: Apache Airflow historical configuration (2.11.0) -- airflow-old: Apache Airflow historical default (2.2.5) -- flower: Flower (v2.2.0) -- tornado: Tornado (v6.5.10) -- linux: Linux permissions and tools (unknown) -- systemd: systemd (v257) -- fhs: Filesystem Hierarchy Standard (3.0) -- docker: Moby Docker (docker-v29.8.1) -- argo-three: Argo Workflows (v3.7.18) -- argo-four: Argo Workflows (v4.1.4) -- argo-docs: Argo Workflows documentation (release-3.7) -- iproute2: iproute2 ss manual (v6.15.0) -- network-namespaces: Linux network namespaces manual (man-pages-5.13) -- prefect-rolling: Prefect documentation (rolling) (unknown) -- dagster-rolling: Dagster documentation (rolling) (unknown) -- airflow-rolling: Apache Airflow documentation (rolling) (unknown) -- temporal-rolling: Temporal documentation (rolling) (unknown) -- flower-rolling: Flower documentation (rolling) (unknown) -- prefect-blocks: Prefect Blocks source (3.1.8) -- fab-source: Apache Airflow FAB provider source (providers-fab/3.9.0) -- airflow-crypto: Apache Airflow crypto source (3.3.2) -- temporal-codec: Temporal Go SDK codec source (v1.28.0) -- fab-rolling: Apache Airflow FAB provider documentation (rolling) (unknown) +# Workflow and agent orchestrators: Prefect, Dagster, Airflow, Temporal, Flower, Argo -Claims (full source mapping in the front matter): +Written against Prefect Basic Auth minimum 3.1.8, Prefect Blocks source 3.1.8, Dagster OSS 1.13.24, Apache Airflow 3.3.2, Apache Airflow historical configuration 2.11.0, Apache Airflow historical default 2.2.5, and Apache Airflow FAB provider source providers-fab/3.9.0, plus 11 more recorded bases; 1 further source is pinned to a commit and 7 carry no recorded version. Metadata reviewed 2026-10-05; documentation checked October 2026. The guide records 62 claims: 3 demonstrated by a check that ran and 59 reasoned from cited sources. -- fronting (REASONED; airflow, argo-docs, temporal-rolling): Keep UI/API origins private behind HTTPS and authentication; use IdP MFA for browsers and separate machine authentication, and constrain worker egress. -- prefect-auth (REASONED; prefect, prefect-rolling): Self-hosted Prefect defaults unauthenticated; Basic Auth from 3.1.8 uses matching server/client auth strings and prompts the UI. -- prefect-key (REASONED; prefect, prefect-rolling): Cloud-only PREFECT_API_KEY takes precedence over PREFECT_API_AUTH_STRING and produces 401 against a self-hosted server. -- prefect-secrets (REASONED; prefect-blocks, prefect-rolling): Protect the auth string and credential-bearing Blocks; Prefect 3.1.8 block-document read routes accept include_secrets, and Secret.get() returns the underlying value. Restrict API and worker access; UI masking does not enforce authorization. -- prefect-port (REASONED; prefect-rolling): The self-hosted Prefect server uses port 4200. -- dagster-bind (REASONED; dagster): Direct dagster-webserver/dev defaults to 127.0.0.1:3000, with flag/environment overrides and free-port fallback when the port is unset or 0; explicitly bind the private origin. -- dagster-helm (REASONED; dagster): The chart passes -h 0.0.0.0 on the Service port, default 80, with ClusterIP Service by default. -- dagster-auth (REASONED; dagster-rolling): OSS webserver has no built-in login or access control; protect all routes with an authenticating proxy. -- airflow-boundary (REASONED; airflow): Airflow assumes authenticated known users and is not designed for untrusted public exposure; deployment managers must keep it private. -- airflow-two-api (REASONED; airflow-two, airflow-old): Airflow 2.11.0 uses [api] auth_backends with session default; before 2.3 the name was auth_backend and 2.2.5 defaulted to deny_all. -- airflow-simple (REASONED; airflow): Airflow 3 defaults to development-only Simple Auth Manager, with configured users/roles and generated passwords printed to logs unless supplied. -- airflow-fab (REASONED; airflow, fab-rolling): For production install FAB and select FabAuthManager through [core] auth_manager; verify the effective manager and use an identity backend with MFA. -- airflow-api (REASONED; airflow, fab-rolling): Airflow 3 public API uses JWT independently of [fab] auth_backends, which selects FAB API backends rather than the auth manager. -- airflow-all-admins (REASONED; airflow): Keep simple_auth_manager_all_admins unset or False; enabling it disables login and grants every visitor admin. -- fab-public-role (REASONED; fab-source): Leave [fab] auth_role_public and legacy AUTH_ROLE_PUBLIC unset; a configured public role can make FAB treat an anonymous visitor as logged in and supply that role's permissions. -- airflow-compose-account (REASONED; airflow-rolling, fab-rolling, airflow): Development Compose selects FAB but defaults to airflow/airflow; set credentials before account creation and update/delete an existing account separately. -- airflow-jwt (REASONED; airflow-rolling, airflow): Replace Compose fallback AIRFLOW__API_AUTH__JWT_SECRET=airflow_jwt_secret and share the strong value with all signers/validators; the public fallback permits token forgery. -- airflow-secret-key (REASONED; airflow-two, airflow-rolling, airflow): Provision the independent secret_key too: [webserver] on 2.11.0 and [api] on 3.3.2. -- airflow-config (REASONED; airflow-two, airflow-rolling, airflow): Keep configuration exposure off with WEBSERVER__EXPOSE_CONFIG on 2.11.0 or API__EXPOSE_CONFIG on 3.3.2. -- airflow-fernet (REASONED; airflow-crypto, airflow-rolling): Protect Connections, Variables, database and Fernet keys; an empty [core] FERNET_KEY selects a no-encryption implementation for new values, while configured comma-separated keys use MultiFernet. Preserve keys needed to decrypt existing values; authorized workloads still use secrets. -- airflow-port (REASONED; airflow-rolling): Compose publishes 8080 on all interfaces; use 127.0.0.1:8080:8080 behind the fronting layer. -- temporal-authorizer (REASONED; temporal-rolling): Empty authorizer selects noopAuthorizer allowing every API request, including administration; configure default authorization with trusted JWT keys and audience. -- temporal-claims (REASONED; temporal-rolling): Empty claimMapper selects a no-op granting system-admin claims; configure both Authorizer and ClaimMapper, with frontend TLS. -- temporal-health (REASONED; temporal-rolling): The built-in default authorizer permits health checks without claims; health is not an authorization discriminator. -- temporal-ui (REASONED; temporal-rolling): UI OIDC auth.enabled is a sibling of providers; TEMPORAL_AUTH_ENABLED plus provider settings gates the UI only, with MFA at the IdP. -- temporal-tls (REASONED; temporal-rolling): Internode and frontend mTLS are separate from UI login and API authorization. -- temporal-payloads (REASONED; temporal-rolling, temporal-codec): Keep plaintext credentials out of persisted inputs/results/history; the Temporal Go SDK v1.28.0 codec HTTP handler accepts encode/decode POSTs without an authentication check, so authenticate and restrict a deployed Codec Server separately. -- temporal-ports (REASONED; temporal-rolling): Frontend gRPC uses 7233 and the Web UI 8233 in the cited CLI server reference. -- flower-bind (REASONED; flower, tornado): Flower defaults to wildcard address and 5555 unless Unix socket, FLOWER_ variables or implicit working-directory flowerconfig.py override; bind loopback explicitly. -- flower-api (REASONED; flower-rolling): Authentication defaults off; unauthenticated HTTP API is disabled unless FLOWER_UNAUTHENTICATED_API=true, which must not be mistaken for dashboard protection. -- flower-basic (REASONED; flower-rolling): Basic auth uses a comma-separated credential list; supply it through FLOWER_BASIC_AUTH or protected config rather than argv and retain private TLS fronting. -- flower-oauth (REASONED; flower-rolling): OAuth needs handler, client key/secret, redirect and allowed-email regex; protect the secret outside argv and prefer an MFA-enforcing provider. -- flower-socket (REASONED; flower, tornado, linux): Flower passes fixed mode 0777; Tornado removes/recreates filesystem sockets on startup, so chmod does not persist. Abstract sockets have no filesystem permission boundary. -- flower-directory (REASONED; linux): Restrict socket directory search with Flower ownership, proxy-only group, mode 0750 and no outsider ACL; directory access is essential because socket mode permits writes. -- flower-capabilities (REASONED; linux, docker): Applicable DAC capabilities bypass directory search checks; Docker defaults include CAP_DAC_OVERRIDE, while user namespaces and idmapped mounts change identity interpretation. -- flower-runtime-dir (REASONED; systemd, fhs): Reapply directory owner/group/mode on creation: /run is cleared at boot and RuntimeDirectory defaults 0755 unless RuntimeDirectoryMode overrides it. -- flower-numeric-ids (REASONED; linux): A proxy sharing the socket directory must hold the required numeric group after namespace/mount mappings; names alone do not establish access. -- argo-bind (REASONED; argo-three, argo-four): Argo Server binds wildcard :2746 with host-dependent IPv4/IPv6 support and no bind-address flag. -- argo-defaults (REASONED; argo-three, argo-four): Both tags default secure=true, auth-mode=client and hsts=true; flags and ARGO_ environment equivalents can override. -- argo-client (REASONED; argo-three, argo-four): Client mode uses caller Kubernetes credentials/RBAC; v4.1.4 validates via SelfSubjectReview and requires Kubernetes 1.28+. -- argo-quickstart (REASONED; argo-three, argo-four): Both tags' minimal/mysql/postgres quick-starts enable server plus client mode while retaining TLS; v4.1.4 telemetry does too. Anonymous callers inherit server permissions. -- argo-quickstart-rbac (REASONED; argo-three, argo-four): Quick-start binds cluster-wide workflow/template and Secret get/create permissions, plus cluster-workflow-template rights; it is demo-only. -- argo-installs (REASONED; argo-three, argo-four): Traced install inputs keep client auth/TLS with cluster-wide server RBAC; namespace-install adds --namespaced and namespace RBAC. Generated install YAML was absent. -- argo-service (REASONED; argo-three, argo-four): Base Service omits type, defaulting to ClusterIP, and maps 2746 to 2746; this does not authenticate reachable callers. -- argo-fallback (REASONED; argo-three, argo-four): Remove server auth mode: adding client/SSO does not prevent anonymous fallback to server credentials; local mode uses the server kubeconfig. -- argo-sso (REASONED; argo-docs, argo-three, argo-four): Enable sso.rbac.enabled and map groups to narrow ServiceAccounts through rbac-rule/precedence; without SSO RBAC, authenticated users share server permissions. -- argo-secrets (REASONED; argo-docs): Store SSO OAuth credentials in referenced Kubernetes Secrets and supply API tokens through protected input, not argv. -- argo-tls (REASONED; argo-three, argo-four): Keep TLS enabled with a trusted certificate Secret; no name generates a self-signed certificate, failed named-Secret loading fails startup, secure=false is plaintext and defeats HSTS. -- argo-metrics (REASONED; argo-three, argo-four): /metrics shares the server listener and auth gate unless ARGO_SERVER_METRICS_AUTH=false; server mode also admits anonymous metrics. -- argo-network (REASONED; argo-docs): Keep 2746 private behind authenticated HTTPS; restrict direct Pod/Service access and scope server, SSO and execution-account permissions. Workflow submission permits arbitrary containers unless constrained. -- verify-inventory (REASONED; flower, argo-three, iproute2, network-namespaces, prefect-rolling): ss inventories only the current namespace, not publication, routing or authentication; inspect publications and external reachability separately. -- verify-prefect (REASONED; prefect, prefect-source, prefect-rolling): Anonymous POST /api/flows/filter returning a JSON list is exposed; fixed is 401 with an authorized list on the same origin. Health/ready GET exemptions are version-dependent. -- verify-dagster (REASONED; dagster-rolling): A RepositoryConnection from /graphql is a read, including empty nodes; GraphQL/transport errors are inconclusive. Test proxy authentication and origin isolation separately. -- verify-airflow (REASONED; airflow-two, airflow-rolling, fab-rolling): FAB 3.9.0 returning 201 with access_token for airflow/airflow is exposed; pair a valid account and anonymous/authenticated API reads at proxy and origin. Airflow 2 uses its own API/auth. -- verify-temporal (REASONED; temporal-rolling): Credential-free workflow listing on 7233 is exposed; require rejection and a matched authorized call, not health, TLS errors or missing namespaces as proof of auth. -- verify-flower (REASONED; flower-rolling): Anonymous UI/API should yield Basic 401 or OAuth login redirect, with a valid-session control; API-disabled is not proof the dashboard is protected. -- verify-socket (REASONED; flower, linux): Directory/socket permissions, outsider denial, reboot persistence and container numeric IDs remain reasoned; no socket or Flower process ran. -- flower-stat-run (DEMONSTRATED; linux): Tmpfs directory-only stat distinguished 0755 and 0750; the 0750 directory initially had only base ACL entries. -- flower-acl-run (DEMONSTRATED; linux): A named nobody search ACL remained invisible in stat mode 750; ls and getfacl exposed the entry. -- flower-mask-run (DEMONSTRATED; linux): Changing the ACL mask hid named search permission; chmod 0750 restored its reported effectiveness. Access as nobody was not tested. -- verify-argo (REASONED; argo-three, argo-four, argo-docs): Anonymous workflow JSON is exposed when server RBAC permits; missing tokens should yield 401 without server mode. A 403 alone is inconclusive; pair authorized 200 and inspect auth modes. -- verify-origin (REASONED; airflow, argo-docs): Any HTTP response from the origin at an untrusted vantage proves publication; use the same responding allowed origin as control and treat connection errors/timeouts as inconclusive. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. These webservers and UIs schedule and trigger arbitrary code execution across your infrastructure, and most ship with no authentication at all. Keep every one of them off the public internet and add auth before @@ -919,3 +476,472 @@ These defaults are checked against Prefect 3.1.8+ for Basic Auth, Dagster 1.13.x - Apache Airflow development Compose FAB selection, administrator credentials and JWT fallback secret (pinned tag 3.3.2): https://raw.githubusercontent.com/apache/airflow/3.3.2/airflow-core/docs/howto/docker-compose/docker-compose.yaml - Apache Airflow webserver secret_key and EXPOSE_CONFIG settings (2.11.0 documentation): https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#secret-key, https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#expose-config - Apache Airflow API secret_key and EXPOSE_CONFIG settings (3.3.2 documentation): https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#secret-key, https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#expose-config + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/workflow-orchestrators.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- prefect: Prefect Basic Auth minimum (3.1.8) +- prefect-blocks: Prefect Blocks source (3.1.8) +- dagster: Dagster OSS (1.13.24) +- airflow: Apache Airflow (3.3.2) +- airflow-two: Apache Airflow historical configuration (2.11.0) +- airflow-old: Apache Airflow historical default (2.2.5) +- fab-source: Apache Airflow FAB provider source (providers-fab/3.9.0) +- airflow-crypto: Apache Airflow crypto source (3.3.2) +- temporal-codec: Temporal Go SDK codec source (v1.28.0) +- flower: Flower (v2.2.0) +- argo-three: Argo Workflows (v3.7.18) +- argo-four: Argo Workflows (v4.1.4) +- argo-docs: Argo Workflows documentation (release-3.7) +- prefect-source: Prefect server source (9e560c9b6df4e19a5109a66e66d461f9facb538d) +- prefect-rolling: Prefect documentation (rolling) (unknown) +- dagster-rolling: Dagster documentation (rolling) (unknown) +- airflow-rolling: Apache Airflow documentation (rolling) (unknown) +- fab-rolling: Apache Airflow FAB provider documentation (rolling) (unknown) +- temporal-rolling: Temporal documentation (rolling) (unknown) +- flower-rolling: Flower documentation (rolling) (unknown) +- tornado: Tornado (v6.5.10) +- docker: Moby Docker (docker-v29.8.1) +- linux: Linux permissions and tools (unknown) +- systemd: systemd (v257) +- fhs: Filesystem Hierarchy Standard (3.0) +- iproute2: iproute2 ss manual (v6.15.0) +- network-namespaces: Linux network namespaces manual (man-pages-5.13) + +Claims (full source mapping in the JSON block below): + +- fronting (REASONED; airflow, argo-docs, temporal-rolling): Keep UI/API origins private behind HTTPS and authentication; use IdP MFA for browsers and separate machine authentication, and constrain worker egress. +- prefect-auth (REASONED; prefect, prefect-rolling): Self-hosted Prefect defaults unauthenticated; Basic Auth from 3.1.8 uses matching server/client auth strings and prompts the UI. +- prefect-key (REASONED; prefect, prefect-rolling): Cloud-only PREFECT_API_KEY takes precedence over PREFECT_API_AUTH_STRING and produces 401 against a self-hosted server. +- prefect-secrets (REASONED; prefect-blocks, prefect-rolling): Protect the auth string and credential-bearing Blocks; Prefect 3.1.8 block-document read routes accept include_secrets, and Secret.get() returns the underlying value. Restrict API and worker access; UI masking does not enforce authorization. +- prefect-port (REASONED; prefect-rolling): The self-hosted Prefect server uses port 4200. +- dagster-bind (REASONED; dagster): Direct dagster-webserver/dev defaults to 127.0.0.1:3000, with flag/environment overrides and free-port fallback when the port is unset or 0; explicitly bind the private origin. +- dagster-helm (REASONED; dagster): The chart passes -h 0.0.0.0 on the Service port, default 80, with ClusterIP Service by default. +- dagster-auth (REASONED; dagster-rolling): OSS webserver has no built-in login or access control; protect all routes with an authenticating proxy. +- airflow-boundary (REASONED; airflow): Airflow assumes authenticated known users and is not designed for untrusted public exposure; deployment managers must keep it private. +- airflow-two-api (REASONED; airflow-two, airflow-old): Airflow 2.11.0 uses [api] auth_backends with session default; before 2.3 the name was auth_backend and 2.2.5 defaulted to deny_all. +- airflow-simple (REASONED; airflow): Airflow 3 defaults to development-only Simple Auth Manager, with configured users/roles and generated passwords printed to logs unless supplied. +- airflow-fab (REASONED; airflow, fab-rolling): For production install FAB and select FabAuthManager through [core] auth_manager; verify the effective manager and use an identity backend with MFA. +- airflow-api (REASONED; airflow, fab-rolling): Airflow 3 public API uses JWT independently of [fab] auth_backends, which selects FAB API backends rather than the auth manager. +- airflow-all-admins (REASONED; airflow): Keep simple_auth_manager_all_admins unset or False; enabling it disables login and grants every visitor admin. +- fab-public-role (REASONED; fab-source): Leave [fab] auth_role_public and legacy AUTH_ROLE_PUBLIC unset; a configured public role can make FAB treat an anonymous visitor as logged in and supply that role's permissions. +- airflow-compose-account (REASONED; airflow-rolling, fab-rolling, airflow): Development Compose selects FAB but defaults to airflow/airflow; set credentials before account creation and update/delete an existing account separately. +- airflow-jwt (REASONED; airflow-rolling, airflow): Replace Compose fallback AIRFLOW__API_AUTH__JWT_SECRET=airflow_jwt_secret and share the strong value with all signers/validators; the public fallback permits token forgery. +- airflow-secret-key (REASONED; airflow-two, airflow-rolling, airflow): Provision the independent secret_key too: [webserver] on 2.11.0 and [api] on 3.3.2. +- airflow-config (REASONED; airflow-two, airflow-rolling, airflow): Keep configuration exposure off with WEBSERVER__EXPOSE_CONFIG on 2.11.0 or API__EXPOSE_CONFIG on 3.3.2. +- airflow-fernet (REASONED; airflow-crypto, airflow-rolling): Protect Connections, Variables, database and Fernet keys; an empty [core] FERNET_KEY selects a no-encryption implementation for new values, while configured comma-separated keys use MultiFernet. Preserve keys needed to decrypt existing values; authorized workloads still use secrets. +- airflow-port (REASONED; airflow-rolling): Compose publishes 8080 on all interfaces; use 127.0.0.1:8080:8080 behind the fronting layer. +- temporal-authorizer (REASONED; temporal-rolling): Empty authorizer selects noopAuthorizer allowing every API request, including administration; configure default authorization with trusted JWT keys and audience. +- temporal-claims (REASONED; temporal-rolling): Empty claimMapper selects a no-op granting system-admin claims; configure both Authorizer and ClaimMapper, with frontend TLS. +- temporal-health (REASONED; temporal-rolling): The built-in default authorizer permits health checks without claims; health is not an authorization discriminator. +- temporal-ui (REASONED; temporal-rolling): UI OIDC auth.enabled is a sibling of providers; TEMPORAL_AUTH_ENABLED plus provider settings gates the UI only, with MFA at the IdP. +- temporal-tls (REASONED; temporal-rolling): Internode and frontend mTLS are separate from UI login and API authorization. +- temporal-payloads (REASONED; temporal-rolling, temporal-codec): Keep plaintext credentials out of persisted inputs/results/history; the Temporal Go SDK v1.28.0 codec HTTP handler accepts encode/decode POSTs without an authentication check, so authenticate and restrict a deployed Codec Server separately. +- temporal-ports (REASONED; temporal-rolling): Frontend gRPC uses 7233 and the Web UI 8233 in the cited CLI server reference. +- flower-bind (REASONED; flower, tornado): Flower defaults to wildcard address and 5555 unless Unix socket, FLOWER_ variables or implicit working-directory flowerconfig.py override; bind loopback explicitly. +- flower-api (REASONED; flower-rolling): Authentication defaults off; unauthenticated HTTP API is disabled unless FLOWER_UNAUTHENTICATED_API=true, which must not be mistaken for dashboard protection. +- flower-basic (REASONED; flower-rolling): Basic auth uses a comma-separated credential list; supply it through FLOWER_BASIC_AUTH or protected config rather than argv and retain private TLS fronting. +- flower-oauth (REASONED; flower-rolling): OAuth needs handler, client key/secret, redirect and allowed-email regex; protect the secret outside argv and prefer an MFA-enforcing provider. +- flower-socket (REASONED; flower, tornado, linux): Flower passes fixed mode 0777; Tornado removes/recreates filesystem sockets on startup, so chmod does not persist. Abstract sockets have no filesystem permission boundary. +- flower-directory (REASONED; linux): Restrict socket directory search with Flower ownership, proxy-only group, mode 0750 and no outsider ACL; directory access is essential because socket mode permits writes. +- flower-capabilities (REASONED; linux, docker): Applicable DAC capabilities bypass directory search checks; Docker defaults include CAP_DAC_OVERRIDE, while user namespaces and idmapped mounts change identity interpretation. +- flower-runtime-dir (REASONED; systemd, fhs): Reapply directory owner/group/mode on creation: /run is cleared at boot and RuntimeDirectory defaults 0755 unless RuntimeDirectoryMode overrides it. +- flower-numeric-ids (REASONED; linux): A proxy sharing the socket directory must hold the required numeric group after namespace/mount mappings; names alone do not establish access. +- argo-bind (REASONED; argo-three, argo-four): Argo Server binds wildcard :2746 with host-dependent IPv4/IPv6 support and no bind-address flag. +- argo-defaults (REASONED; argo-three, argo-four): Both tags default secure=true, auth-mode=client and hsts=true; flags and ARGO_ environment equivalents can override. +- argo-client (REASONED; argo-three, argo-four): Client mode uses caller Kubernetes credentials/RBAC; v4.1.4 validates via SelfSubjectReview and requires Kubernetes 1.28+. +- argo-quickstart (REASONED; argo-three, argo-four): Both tags' minimal/mysql/postgres quick-starts enable server plus client mode while retaining TLS; v4.1.4 telemetry does too. Anonymous callers inherit server permissions. +- argo-quickstart-rbac (REASONED; argo-three, argo-four): Quick-start binds cluster-wide workflow/template and Secret get/create permissions, plus cluster-workflow-template rights; it is demo-only. +- argo-installs (REASONED; argo-three, argo-four): Traced install inputs keep client auth/TLS with cluster-wide server RBAC; namespace-install adds --namespaced and namespace RBAC. Generated install YAML was absent. +- argo-service (REASONED; argo-three, argo-four): Base Service omits type, defaulting to ClusterIP, and maps 2746 to 2746; this does not authenticate reachable callers. +- argo-fallback (REASONED; argo-three, argo-four): Remove server auth mode: adding client/SSO does not prevent anonymous fallback to server credentials; local mode uses the server kubeconfig. +- argo-sso (REASONED; argo-docs, argo-three, argo-four): Enable sso.rbac.enabled and map groups to narrow ServiceAccounts through rbac-rule/precedence; without SSO RBAC, authenticated users share server permissions. +- argo-secrets (REASONED; argo-docs): Store SSO OAuth credentials in referenced Kubernetes Secrets and supply API tokens through protected input, not argv. +- argo-tls (REASONED; argo-three, argo-four): Keep TLS enabled with a trusted certificate Secret; no name generates a self-signed certificate, failed named-Secret loading fails startup, secure=false is plaintext and defeats HSTS. +- argo-metrics (REASONED; argo-three, argo-four): /metrics shares the server listener and auth gate unless ARGO_SERVER_METRICS_AUTH=false; server mode also admits anonymous metrics. +- argo-network (REASONED; argo-docs): Keep 2746 private behind authenticated HTTPS; restrict direct Pod/Service access and scope server, SSO and execution-account permissions. Workflow submission permits arbitrary containers unless constrained. +- verify-inventory (REASONED; flower, argo-three, iproute2, network-namespaces, prefect-rolling): ss inventories only the current namespace, not publication, routing or authentication; inspect publications and external reachability separately. +- verify-prefect (REASONED; prefect, prefect-source, prefect-rolling): Anonymous POST /api/flows/filter returning a JSON list is exposed; fixed is 401 with an authorized list on the same origin. Health/ready GET exemptions are version-dependent. +- verify-dagster (REASONED; dagster-rolling): A RepositoryConnection from /graphql is a read, including empty nodes; GraphQL/transport errors are inconclusive. Test proxy authentication and origin isolation separately. +- verify-airflow (REASONED; airflow-two, airflow-rolling, fab-rolling): FAB 3.9.0 returning 201 with access_token for airflow/airflow is exposed; pair a valid account and anonymous/authenticated API reads at proxy and origin. Airflow 2 uses its own API/auth. +- verify-temporal (REASONED; temporal-rolling): Credential-free workflow listing on 7233 is exposed; require rejection and a matched authorized call, not health, TLS errors or missing namespaces as proof of auth. +- verify-flower (REASONED; flower-rolling): Anonymous UI/API should yield Basic 401 or OAuth login redirect, with a valid-session control; API-disabled is not proof the dashboard is protected. +- verify-socket (REASONED; flower, linux): Directory/socket permissions, outsider denial, reboot persistence and container numeric IDs remain reasoned; no socket or Flower process ran. +- flower-stat-run (DEMONSTRATED; linux): Tmpfs directory-only stat distinguished 0755 and 0750; the 0750 directory initially had only base ACL entries. +- flower-acl-run (DEMONSTRATED; linux): A named nobody search ACL remained invisible in stat mode 750; ls and getfacl exposed the entry. +- flower-mask-run (DEMONSTRATED; linux): Changing the ACL mask hid named search permission; chmod 0750 restored its reported effectiveness. Access as nobody was not tested. +- verify-argo (REASONED; argo-three, argo-four, argo-docs): Anonymous workflow JSON is exposed when server RBAC permits; missing tokens should yield 401 without server mode. A 403 alone is inconclusive; pair authorized 200 and inspect auth modes. +- verify-origin (REASONED; airflow, argo-docs): Any HTTP response from the origin at an untrusted vantage proves publication; use the same responding allowed origin as control and treat connection errors/timeouts as inconclusive. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-10", + "body_sha256": "fa18bb5bca5189881826e754d7d45b7a5179143b73c35dd0f654f46b3e7945b1", + "components": { + "prefect": { + "name": "Prefect Basic Auth minimum", + "basis": "3.1.8", + "sources": { + "s12416d1a0d69": "https://github.com/PrefectHQ/prefect/releases/tag/3.1.8" + } + }, + "prefect-blocks": { + "name": "Prefect Blocks source", + "basis": "3.1.8", + "sources": { + "s56081101d16a": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/server/api/block_documents.py#L47-L70", + "s1eae2c5f9f5e": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/server/api/block_documents.py#L101-L119", + "s46e69a2090c4": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/blocks/system.py#L154-L159" + } + }, + "dagster": { + "name": "Dagster OSS", + "basis": "1.13.24", + "sources": { + "sab1b42d92f27": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L41-L42", + "s6bc57f86dfe1": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L81-L96", + "s2455602a504b": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L306-L311", + "s3c7d8f17f245": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster/dagster/_cli/dev.py#L251-L252", + "s469c458c4a71": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L339-L351", + "s695dfd19dcca": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/templates/helpers/_deployment-webserver.tpl#L86-L90", + "sf6f027e96a0d": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/templates/helpers/_helpers.tpl#L55", + "seda3a1b24425": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/values.yaml#L54-L58" + } + }, + "airflow": { + "name": "Apache Airflow", + "basis": "3.3.2", + "sources": { + "s1d0b86fe4334": "https://airflow.apache.org/docs/apache-airflow/3.3.2/security/security_model.html", + "sad02a49561a0": "https://airflow.apache.org/docs/apache-airflow/3.3.2/core-concepts/auth-manager/simple/index.html", + "s3be3e4616150": "https://airflow.apache.org/docs/apache-airflow/3.3.2/core-concepts/auth-manager/index.html", + "s78c661193a1a": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#simple-auth-manager-all-admins", + "s963b49f58a13": "https://airflow.apache.org/docs/apache-airflow/3.3.2/security/api.html", + "s705f1eb5de00": "https://raw.githubusercontent.com/apache/airflow/3.3.2/airflow-core/docs/howto/docker-compose/docker-compose.yaml", + "sdfcfd543928c": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#secret-key", + "s4ed64009ed8f": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#expose-config" + } + }, + "airflow-two": { + "name": "Apache Airflow historical configuration", + "basis": "2.11.0", + "sources": { + "sb97f3044e215": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#auth-backends", + "sabb71378ad74": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#secret-key", + "s6813801b7880": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#expose-config" + } + }, + "airflow-old": { + "name": "Apache Airflow historical default", + "basis": "2.2.5", + "sources": { + "s4e72b1f51038": "https://airflow.apache.org/docs/apache-airflow/2.2.5/configurations-ref.html#auth-backend" + } + }, + "fab-source": { + "name": "Apache Airflow FAB provider source", + "basis": "providers-fab/3.9.0", + "sources": { + "s3c94f0f7176a": "https://github.com/apache/airflow/blob/providers-fab/3.9.0/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py#L327-L381", + "s5552a28ab9d8": "https://github.com/apache/airflow/blob/providers-fab/3.9.0/providers/fab/pyproject.toml#L75-L81" + } + }, + "airflow-crypto": { + "name": "Apache Airflow crypto source", + "basis": "3.3.2", + "sources": { + "sdeaf2f54c054": "https://github.com/apache/airflow/blob/3.3.2/airflow-core/src/airflow/models/crypto.py#L47-L68", + "s70ea7631e820": "https://github.com/apache/airflow/blob/3.3.2/airflow-core/src/airflow/models/crypto.py#L154-L173" + } + }, + "temporal-codec": { + "name": "Temporal Go SDK codec source", + "basis": "v1.28.0", + "sources": { + "s24435f531da0": "https://github.com/temporalio/sdk-go/blob/v1.28.0/converter/codec.go#L284-L350" + } + }, + "flower": { + "name": "Flower", + "basis": "v2.2.0", + "sources": { + "s25760fae7152": "https://github.com/mher/flower/blob/v2.2.0/flower/options.py#L7-L15", + "s1f72a8415337": "https://github.com/mher/flower/blob/v2.2.0/flower/options.py#L56-L57", + "s4dd518442b7f": "https://github.com/mher/flower/blob/v2.2.0/flower/command.py#L39-L91", + "s4e49ebc71dc1": "https://github.com/mher/flower/blob/v2.2.0/flower/app.py#L71-L76", + "s0fe6c77e9c9d": "https://github.com/mher/flower/blob/v2.2.0/flower/app.py#L85-L89" + } + }, + "argo-three": { + "name": "Argo Workflows", + "basis": "v3.7.18", + "sources": { + "s262098553d2f": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/cmd/argo/commands/server.go#L191-L229", + "sbddb9f07a301": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/apiserver/argoserver.go#L263-L282", + "s2ca4f14c3164": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/gatekeeper.go#L166-L218", + "sfbb904619bfa": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/overlays/argo-server-deployment.yaml#L1-L15", + "s0ac72aa1a86c": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterole.yaml#L1-L65", + "s1689f25010be": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/cluster-workflow-template-rbac.yaml#L1-L58", + "sf0c3a52d3302": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/Makefile#L493-L515", + "sfdcdf614ee37": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/base/argo-server/argo-server-deployment.yaml#L14-L34", + "sc7ca3a37b16c": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/overlays/argo-server-deployment.yaml#L1-L4", + "s01f102470d97": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/base/argo-server/argo-server-service.yaml#L1-L11", + "s49cdaf0691c0": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/mode.go#L33-L44", + "sdb4a0c18b7c6": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/cmd/argo/commands/server.go#L113-L137", + "s36759aec55fa": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/apiserver/argoserver.go#L430-L449", + "s862033e52c9b": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-minimal.yaml#L3613-L3664", + "s73e9beeef775": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-mysql.yaml#L3678-L3729", + "s476ff15db454": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-postgres.yaml#L3677-L3728", + "sa4b2fa875915": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterolebinding.yaml#L1-L11", + "s48db5ba07d23": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/kustomization.yaml#L4-L5", + "se3ffdf2973a6": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/argo-server-rbac/argo-server-role.yaml#L1-L65", + "sb901e7f87b54": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/argo-server-rbac/argo-server-rolebinding.yaml#L1-L11", + "s49a8855b6da2": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/docs/argo-server-auth-mode.md#L3-L9", + "s64e74b0dbe95": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/gatekeeper.go#L106-L121" + } + }, + "argo-four": { + "name": "Argo Workflows", + "basis": "v4.1.4", + "sources": { + "s774215022573": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/cmd/argo/commands/server.go#L195-L239", + "s0f074a9ac21e": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/apiserver/argoserver.go#L299-L320", + "sb284f93cde9c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/gatekeeper.go#L189-L244", + "s0871a1dd9b13": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/docs/argo-server-auth-mode.md#L3-L9", + "sa7b9db8384be": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start/base/overlays/argo-server-deployment.yaml#L1-L15", + "s79648b46d3f5": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-telemetry.yaml#L192919-L192970", + "sd2706d04971d": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterole.yaml#L1-L67", + "sd920da4ad4b7": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/docs/quick-start.md#L3-L34", + "s144b824252c1": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/Makefile#L608-L634", + "s51645bfbc47c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/base/argo-server/argo-server-deployment.yaml#L14-L34", + "s5188f6f2efbb": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/overlays/argo-server-deployment.yaml#L1-L4", + "s639e9fe0024e": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/base/argo-server/argo-server-service.yaml#L1-L11", + "s4613c15c72af": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/mode.go#L33-L44", + "s0b16f1ac50df": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/cmd/argo/commands/server.go#L119-L143", + "sa120f74372eb": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/apiserver/argoserver.go#L476-L494", + "s6c08a5f34f2c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-minimal.yaml#L192772-L192823", + "sa0bf6187d1db": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-mysql.yaml#L192838-L192889", + "s31f4a149576b": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-postgres.yaml#L192837-L192888", + "s3be9b03e29be": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterolebinding.yaml#L1-L11", + "sc263bd937cf0": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start/base/kustomization.yaml#L4-L5", + "s1b134f96428f": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/argo-server-rbac/argo-server-role.yaml#L1-L67", + "s2418ee1aa49d": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/argo-server-rbac/argo-server-rolebinding.yaml#L1-L11", + "se758d83aaf1f": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/gatekeeper.go#L133-L148" + } + }, + "argo-docs": { + "name": "Argo Workflows documentation", + "basis": "release-3.7", + "sources": { + "s6c8e78288632": "https://argo-workflows.readthedocs.io/en/release-3.7/security/", + "s9898d4653a47": "https://argo-workflows.readthedocs.io/en/release-3.7/argo-server-sso/", + "s96145eacf345": "https://argo-workflows.readthedocs.io/en/release-3.7/rest-examples/" + } + }, + "prefect-source": { + "name": "Prefect server source", + "basis": "9e560c9b6df4e19a5109a66e66d461f9facb538d", + "sources": { + "sa4574c7ed80f": "https://github.com/PrefectHQ/prefect/blob/9e560c9b6df4e19a5109a66e66d461f9facb538d/src/prefect/server/api/server.py" + } + }, + "prefect-rolling": { + "name": "Prefect documentation (rolling)", + "basis": "unknown", + "sources": { + "sbe64d51110a2": "https://docs.prefect.io/v3/how-to-guides/self-hosted/server-cli", + "s0ef73a227667": "https://docs.prefect.io/v3/advanced/security-settings" + } + }, + "dagster-rolling": { + "name": "Dagster documentation (rolling)", + "basis": "unknown", + "sources": { + "s4c04d5da5257": "https://docs.dagster.io/guides/operate/webserver" + } + }, + "airflow-rolling": { + "name": "Apache Airflow documentation (rolling)", + "basis": "unknown", + "sources": { + "s2d1891641c91": "https://airflow.apache.org/docs/apache-airflow/stable/security/", + "s39bfd91c507b": "https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html" + } + }, + "fab-rolling": { + "name": "Apache Airflow FAB provider documentation (rolling)", + "basis": "unknown", + "sources": { + "s989f09d05161": "https://airflow.apache.org/docs/apache-airflow-providers-fab/stable/auth-manager/api-authentication.html" + } + }, + "temporal-rolling": { + "name": "Temporal documentation (rolling)", + "basis": "unknown", + "sources": { + "s12f2c64ad396": "https://docs.temporal.io/cli/command-reference/server", + "s06d4f0dbd0c8": "https://docs.temporal.io/self-hosted-guide/security", + "sa9d5614385ca": "https://docs.temporal.io/references/web-ui-configuration" + } + }, + "flower-rolling": { + "name": "Flower documentation (rolling)", + "basis": "unknown", + "sources": { + "sb1ba69f79c9b": "https://flower.readthedocs.io/en/latest/config.html" + } + }, + "tornado": { + "name": "Tornado", + "basis": "v6.5.10", + "sources": { + "sc503b4425831": "https://github.com/tornadoweb/tornado/blob/v6.5.10/tornado/netutil.py#L72-L73", + "s737c51cccd00": "https://github.com/tornadoweb/tornado/blob/v6.5.10/tornado/netutil.py#L215-L228" + } + }, + "docker": { + "name": "Moby Docker", + "basis": "docker-v29.8.1", + "sources": { + "s1f268e247a1f": "https://github.com/moby/moby/blob/docker-v29.8.1/daemon/pkg/oci/caps/defaults.go#L4-L7" + } + }, + "linux": { + "name": "Linux permissions and tools", + "basis": "unknown", + "sources": { + "s8ffd93b0efa4": "https://man7.org/linux/man-pages/man7/unix.7.html", + "sfb53d2819cd0": "https://man7.org/linux/man-pages/man7/path_resolution.7.html", + "s31f41c54750c": "https://man7.org/linux/man-pages/man5/acl.5.html", + "s3cf3d5be0f64": "https://man7.org/linux/man-pages/man7/user_namespaces.7.html", + "sa5ccbfea6e16": "https://www.kernel.org/doc/html/latest/filesystems/idmappings.html", + "s08902025e5f9": "https://man7.org/linux/man-pages/man1/getfacl.1.html", + "sa0d3793fabb7": "https://man7.org/linux/man-pages/man1/stat.1.html" + } + }, + "systemd": { + "name": "systemd", + "basis": "v257", + "sources": { + "s48ece26c16d3": "https://github.com/systemd/systemd/blob/v257/man/systemd.exec.xml#L1630-L1640" + } + }, + "fhs": { + "name": "Filesystem Hierarchy Standard", + "basis": "3.0", + "sources": { + "seafd8219f367": "https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch03s15.html" + } + }, + "iproute2": { + "name": "iproute2 ss manual", + "basis": "v6.15.0", + "sources": { + "s6cc7f285a0c7": "https://github.com/iproute2/iproute2/blob/v6.15.0/man/man8/ss.8#L358-L359", + "s9fa9a260f1de": "https://github.com/iproute2/iproute2/blob/v6.15.0/man/man8/ss.8#L7-L12" + } + }, + "network-namespaces": { + "name": "Linux network namespaces manual", + "basis": "man-pages-5.13", + "sources": { + "s427c923e346f": "https://github.com/mkerrisk/man-pages/blob/man-pages-5.13/man7/network_namespaces.7#L30-L40" + } + } + }, + "claims": { + "fronting": {"text": "Keep UI/API origins private behind HTTPS and authentication; use IdP MFA for browsers and separate machine authentication, and constrain worker egress.", "components": ["airflow", "argo-docs", "temporal-rolling"], "sources": ["airflow:s1d0b86fe4334", "temporal-rolling:s06d4f0dbd0c8", "argo-docs:s6c8e78288632"], "status": "REASONED"}, + "prefect-auth": {"text": "Self-hosted Prefect defaults unauthenticated; Basic Auth from 3.1.8 uses matching server/client auth strings and prompts the UI.", "components": ["prefect", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, + "prefect-key": {"text": "Cloud-only PREFECT_API_KEY takes precedence over PREFECT_API_AUTH_STRING and produces 401 against a self-hosted server.", "components": ["prefect", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, + "prefect-secrets": {"text": "Protect the auth string and credential-bearing Blocks; Prefect 3.1.8 block-document read routes accept include_secrets, and Secret.get() returns the underlying value. Restrict API and worker access; UI masking does not enforce authorization.", "components": ["prefect-blocks", "prefect-rolling"], "sources": ["prefect-blocks:s56081101d16a", "prefect-blocks:s1eae2c5f9f5e", "prefect-blocks:s46e69a2090c4", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, + "prefect-port": {"text": "The self-hosted Prefect server uses port 4200.", "components": ["prefect-rolling"], "sources": ["prefect-rolling:sbe64d51110a2"], "status": "REASONED"}, + "dagster-bind": {"text": "Direct dagster-webserver/dev defaults to 127.0.0.1:3000, with flag/environment overrides and free-port fallback when the port is unset or 0; explicitly bind the private origin.", "components": ["dagster"], "sources": ["dagster:sab1b42d92f27", "dagster:s6bc57f86dfe1", "dagster:s2455602a504b", "dagster:s3c7d8f17f245", "dagster:s469c458c4a71"], "status": "REASONED"}, + "dagster-helm": {"text": "The chart passes -h 0.0.0.0 on the Service port, default 80, with ClusterIP Service by default.", "components": ["dagster"], "sources": ["dagster:s695dfd19dcca", "dagster:sf6f027e96a0d", "dagster:seda3a1b24425"], "status": "REASONED"}, + "dagster-auth": {"text": "OSS webserver has no built-in login or access control; protect all routes with an authenticating proxy.", "components": ["dagster-rolling"], "sources": ["dagster-rolling:s4c04d5da5257"], "status": "REASONED"}, + "airflow-boundary": {"text": "Airflow assumes authenticated known users and is not designed for untrusted public exposure; deployment managers must keep it private.", "components": ["airflow"], "sources": ["airflow:s1d0b86fe4334"], "status": "REASONED"}, + "airflow-two-api": {"text": "Airflow 2.11.0 uses [api] auth_backends with session default; before 2.3 the name was auth_backend and 2.2.5 defaulted to deny_all.", "components": ["airflow-two", "airflow-old"], "sources": ["airflow-two:sb97f3044e215", "airflow-old:s4e72b1f51038"], "status": "REASONED"}, + "airflow-simple": {"text": "Airflow 3 defaults to development-only Simple Auth Manager, with configured users/roles and generated passwords printed to logs unless supplied.", "components": ["airflow"], "sources": ["airflow:sad02a49561a0", "airflow:s3be3e4616150"], "status": "REASONED"}, + "airflow-fab": {"text": "For production install FAB and select FabAuthManager through [core] auth_manager; verify the effective manager and use an identity backend with MFA.", "components": ["airflow", "fab-rolling"], "sources": ["airflow:s3be3e4616150", "fab-rolling:s989f09d05161"], "status": "REASONED"}, + "airflow-api": {"text": "Airflow 3 public API uses JWT independently of [fab] auth_backends, which selects FAB API backends rather than the auth manager.", "components": ["airflow", "fab-rolling"], "sources": ["airflow:s3be3e4616150", "fab-rolling:s989f09d05161", "airflow:s963b49f58a13"], "status": "REASONED"}, + "airflow-all-admins": {"text": "Keep simple_auth_manager_all_admins unset or False; enabling it disables login and grants every visitor admin.", "components": ["airflow"], "sources": ["airflow:sad02a49561a0", "airflow:s78c661193a1a"], "status": "REASONED"}, + "fab-public-role": {"text": "Leave [fab] auth_role_public and legacy AUTH_ROLE_PUBLIC unset; a configured public role can make FAB treat an anonymous visitor as logged in and supply that role's permissions.", "components": ["fab-source"], "sources": ["fab-source:s3c94f0f7176a", "fab-source:s5552a28ab9d8"], "status": "REASONED"}, + "airflow-compose-account": {"text": "Development Compose selects FAB but defaults to airflow/airflow; set credentials before account creation and update/delete an existing account separately.", "components": ["airflow-rolling", "fab-rolling", "airflow"], "sources": ["airflow-rolling:s39bfd91c507b", "fab-rolling:s989f09d05161", "airflow:s705f1eb5de00"], "status": "REASONED"}, + "airflow-jwt": {"text": "Replace Compose fallback AIRFLOW__API_AUTH__JWT_SECRET=airflow_jwt_secret and share the strong value with all signers/validators; the public fallback permits token forgery.", "components": ["airflow-rolling", "airflow"], "sources": ["airflow-rolling:s39bfd91c507b", "airflow:s705f1eb5de00"], "status": "REASONED"}, + "airflow-secret-key": {"text": "Provision the independent secret_key too: [webserver] on 2.11.0 and [api] on 3.3.2.", "components": ["airflow-two", "airflow-rolling", "airflow"], "sources": ["airflow-rolling:s2d1891641c91", "airflow-two:sabb71378ad74", "airflow:sdfcfd543928c"], "status": "REASONED"}, + "airflow-config": {"text": "Keep configuration exposure off with WEBSERVER__EXPOSE_CONFIG on 2.11.0 or API__EXPOSE_CONFIG on 3.3.2.", "components": ["airflow-two", "airflow-rolling", "airflow"], "sources": ["airflow-rolling:s2d1891641c91", "airflow-two:s6813801b7880", "airflow:s4ed64009ed8f"], "status": "REASONED"}, + "airflow-fernet": {"text": "Protect Connections, Variables, database and Fernet keys; an empty [core] FERNET_KEY selects a no-encryption implementation for new values, while configured comma-separated keys use MultiFernet. Preserve keys needed to decrypt existing values; authorized workloads still use secrets.", "components": ["airflow-crypto", "airflow-rolling"], "sources": ["airflow-crypto:sdeaf2f54c054", "airflow-crypto:s70ea7631e820", "airflow-rolling:s2d1891641c91"], "status": "REASONED"}, + "airflow-port": {"text": "Compose publishes 8080 on all interfaces; use 127.0.0.1:8080:8080 behind the fronting layer.", "components": ["airflow-rolling"], "sources": ["airflow-rolling:s39bfd91c507b"], "status": "REASONED"}, + "temporal-authorizer": {"text": "Empty authorizer selects noopAuthorizer allowing every API request, including administration; configure default authorization with trusted JWT keys and audience.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, + "temporal-claims": {"text": "Empty claimMapper selects a no-op granting system-admin claims; configure both Authorizer and ClaimMapper, with frontend TLS.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, + "temporal-health": {"text": "The built-in default authorizer permits health checks without claims; health is not an authorization discriminator.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, + "temporal-ui": {"text": "UI OIDC auth.enabled is a sibling of providers; TEMPORAL_AUTH_ENABLED plus provider settings gates the UI only, with MFA at the IdP.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:sa9d5614385ca"], "status": "REASONED"}, + "temporal-tls": {"text": "Internode and frontend mTLS are separate from UI login and API authorization.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, + "temporal-payloads": {"text": "Keep plaintext credentials out of persisted inputs/results/history; the Temporal Go SDK v1.28.0 codec HTTP handler accepts encode/decode POSTs without an authentication check, so authenticate and restrict a deployed Codec Server separately.", "components": ["temporal-rolling", "temporal-codec"], "sources": ["temporal-rolling:s06d4f0dbd0c8", "temporal-codec:s24435f531da0"], "status": "REASONED"}, + "temporal-ports": {"text": "Frontend gRPC uses 7233 and the Web UI 8233 in the cited CLI server reference.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s12f2c64ad396"], "status": "REASONED"}, + "flower-bind": {"text": "Flower defaults to wildcard address and 5555 unless Unix socket, FLOWER_ variables or implicit working-directory flowerconfig.py override; bind loopback explicitly.", "components": ["flower", "tornado"], "sources": ["flower:s25760fae7152", "flower:s1f72a8415337", "flower:s4dd518442b7f", "flower:s4e49ebc71dc1", "tornado:sc503b4425831"], "status": "REASONED"}, + "flower-api": {"text": "Authentication defaults off; unauthenticated HTTP API is disabled unless FLOWER_UNAUTHENTICATED_API=true, which must not be mistaken for dashboard protection.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, + "flower-basic": {"text": "Basic auth uses a comma-separated credential list; supply it through FLOWER_BASIC_AUTH or protected config rather than argv and retain private TLS fronting.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, + "flower-oauth": {"text": "OAuth needs handler, client key/secret, redirect and allowed-email regex; protect the secret outside argv and prefer an MFA-enforcing provider.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, + "flower-socket": {"text": "Flower passes fixed mode 0777; Tornado removes/recreates filesystem sockets on startup, so chmod does not persist. Abstract sockets have no filesystem permission boundary.", "components": ["flower", "tornado", "linux"], "sources": ["flower:s0fe6c77e9c9d", "tornado:s737c51cccd00", "linux:s8ffd93b0efa4"], "status": "REASONED"}, + "flower-directory": {"text": "Restrict socket directory search with Flower ownership, proxy-only group, mode 0750 and no outsider ACL; directory access is essential because socket mode permits writes.", "components": ["linux"], "sources": ["linux:sfb53d2819cd0", "linux:s8ffd93b0efa4", "linux:s31f41c54750c"], "status": "REASONED"}, + "flower-capabilities": {"text": "Applicable DAC capabilities bypass directory search checks; Docker defaults include CAP_DAC_OVERRIDE, while user namespaces and idmapped mounts change identity interpretation.", "components": ["linux", "docker"], "sources": ["linux:s3cf3d5be0f64", "linux:sa5ccbfea6e16", "docker:s1f268e247a1f"], "status": "REASONED"}, + "flower-runtime-dir": {"text": "Reapply directory owner/group/mode on creation: /run is cleared at boot and RuntimeDirectory defaults 0755 unless RuntimeDirectoryMode overrides it.", "components": ["systemd", "fhs"], "sources": ["systemd:s48ece26c16d3", "fhs:seafd8219f367"], "status": "REASONED"}, + "flower-numeric-ids": {"text": "A proxy sharing the socket directory must hold the required numeric group after namespace/mount mappings; names alone do not establish access.", "components": ["linux"], "sources": ["linux:s3cf3d5be0f64", "linux:sa5ccbfea6e16"], "status": "REASONED"}, + "argo-bind": {"text": "Argo Server binds wildcard :2746 with host-dependent IPv4/IPv6 support and no bind-address flag.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s262098553d2f", "argo-four:s774215022573", "argo-three:sbddb9f07a301", "argo-four:s0f074a9ac21e"], "status": "REASONED"}, + "argo-defaults": {"text": "Both tags default secure=true, auth-mode=client and hsts=true; flags and ARGO_ environment equivalents can override.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s262098553d2f", "argo-four:s774215022573"], "status": "REASONED"}, + "argo-client": {"text": "Client mode uses caller Kubernetes credentials/RBAC; v4.1.4 validates via SelfSubjectReview and requires Kubernetes 1.28+.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c", "argo-four:s0871a1dd9b13"], "status": "REASONED"}, + "argo-quickstart": {"text": "Both tags' minimal/mysql/postgres quick-starts enable server plus client mode while retaining TLS; v4.1.4 telemetry does too. Anonymous callers inherit server permissions.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sfbb904619bfa", "argo-four:sa7b9db8384be", "argo-four:s79648b46d3f5", "argo-three:s862033e52c9b", "argo-three:s73e9beeef775", "argo-three:s476ff15db454", "argo-three:s262098553d2f", "argo-three:s49cdaf0691c0", "argo-three:s2ca4f14c3164", "argo-four:s6c08a5f34f2c", "argo-four:sa0bf6187d1db", "argo-four:s31f4a149576b", "argo-four:s774215022573", "argo-four:s4613c15c72af", "argo-four:sb284f93cde9c"], "status": "REASONED"}, + "argo-quickstart-rbac": {"text": "Quick-start binds cluster-wide workflow/template and Secret get/create permissions, plus cluster-workflow-template rights; it is demo-only.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s0ac72aa1a86c", "argo-four:sd2706d04971d", "argo-three:s1689f25010be", "argo-four:sd920da4ad4b7", "argo-three:sa4b2fa875915", "argo-three:s48db5ba07d23", "argo-four:s3be9b03e29be", "argo-four:sc263bd937cf0"], "status": "REASONED"}, + "argo-installs": {"text": "Traced install inputs keep client auth/TLS with cluster-wide server RBAC; namespace-install adds --namespaced and namespace RBAC. Generated install YAML was absent.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sf0c3a52d3302", "argo-four:s144b824252c1", "argo-three:sfdcdf614ee37", "argo-four:s51645bfbc47c", "argo-three:sc7ca3a37b16c", "argo-four:s5188f6f2efbb", "argo-three:s262098553d2f", "argo-three:s0ac72aa1a86c", "argo-three:sa4b2fa875915", "argo-three:se3ffdf2973a6", "argo-three:sb901e7f87b54", "argo-four:s774215022573", "argo-four:sd2706d04971d", "argo-four:s3be9b03e29be", "argo-four:s1b134f96428f", "argo-four:s2418ee1aa49d"], "status": "REASONED"}, + "argo-service": {"text": "Base Service omits type, defaulting to ClusterIP, and maps 2746 to 2746; this does not authenticate reachable callers.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s01f102470d97", "argo-four:s639e9fe0024e"], "status": "REASONED"}, + "argo-fallback": {"text": "Remove server auth mode: adding client/SSO does not prevent anonymous fallback to server credentials; local mode uses the server kubeconfig.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s49cdaf0691c0", "argo-four:s4613c15c72af", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c", "argo-four:s0871a1dd9b13", "argo-three:s49a8855b6da2"], "status": "REASONED"}, + "argo-sso": {"text": "Enable sso.rbac.enabled and map groups to narrow ServiceAccounts through rbac-rule/precedence; without SSO RBAC, authenticated users share server permissions.", "components": ["argo-docs", "argo-three", "argo-four"], "sources": ["argo-docs:s9898d4653a47", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c"], "status": "REASONED"}, + "argo-secrets": {"text": "Store SSO OAuth credentials in referenced Kubernetes Secrets and supply API tokens through protected input, not argv.", "components": ["argo-docs"], "sources": ["argo-docs:s9898d4653a47", "argo-docs:s96145eacf345"], "status": "REASONED"}, + "argo-tls": {"text": "Keep TLS enabled with a trusted certificate Secret; no name generates a self-signed certificate, failed named-Secret loading fails startup, secure=false is plaintext and defeats HSTS.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sdb4a0c18b7c6", "argo-four:s0b16f1ac50df", "argo-three:s36759aec55fa", "argo-four:sa120f74372eb"], "status": "REASONED"}, + "argo-metrics": {"text": "/metrics shares the server listener and auth gate unless ARGO_SERVER_METRICS_AUTH=false; server mode also admits anonymous metrics.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s36759aec55fa", "argo-four:sa120f74372eb", "argo-three:s49cdaf0691c0", "argo-three:s2ca4f14c3164", "argo-four:s4613c15c72af", "argo-four:sb284f93cde9c", "argo-three:s64e74b0dbe95", "argo-four:se758d83aaf1f"], "status": "REASONED"}, + "argo-network": {"text": "Keep 2746 private behind authenticated HTTPS; restrict direct Pod/Service access and scope server, SSO and execution-account permissions. Workflow submission permits arbitrary containers unless constrained.", "components": ["argo-docs"], "sources": ["argo-docs:s6c8e78288632"], "status": "REASONED"}, + "verify-inventory": {"text": "ss inventories only the current namespace, not publication, routing or authentication; inspect publications and external reachability separately.", "components": ["flower", "argo-three", "iproute2", "network-namespaces", "prefect-rolling"], "sources": ["prefect-rolling:sbe64d51110a2", "flower:s25760fae7152", "argo-three:sbddb9f07a301", "iproute2:s6cc7f285a0c7", "network-namespaces:s427c923e346f", "iproute2:s9fa9a260f1de"], "status": "REASONED", "verify": [1]}, + "verify-prefect": {"text": "Anonymous POST /api/flows/filter returning a JSON list is exposed; fixed is 401 with an authorized list on the same origin. Health/ready GET exemptions are version-dependent.", "components": ["prefect", "prefect-source", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-source:sa4574c7ed80f", "prefect-rolling:s0ef73a227667"], "status": "REASONED", "verify": [1]}, + "verify-dagster": {"text": "A RepositoryConnection from /graphql is a read, including empty nodes; GraphQL/transport errors are inconclusive. Test proxy authentication and origin isolation separately.", "components": ["dagster-rolling"], "sources": ["dagster-rolling:s4c04d5da5257"], "status": "REASONED", "verify": [1]}, + "verify-airflow": {"text": "FAB 3.9.0 returning 201 with access_token for airflow/airflow is exposed; pair a valid account and anonymous/authenticated API reads at proxy and origin. Airflow 2 uses its own API/auth.", "components": ["airflow-two", "airflow-rolling", "fab-rolling"], "sources": ["fab-rolling:s989f09d05161", "airflow-rolling:s39bfd91c507b", "airflow-two:sb97f3044e215"], "status": "REASONED", "verify": [1]}, + "verify-temporal": {"text": "Credential-free workflow listing on 7233 is exposed; require rejection and a matched authorized call, not health, TLS errors or missing namespaces as proof of auth.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8", "temporal-rolling:s12f2c64ad396"], "status": "REASONED", "verify": [1]}, + "verify-flower": {"text": "Anonymous UI/API should yield Basic 401 or OAuth login redirect, with a valid-session control; API-disabled is not proof the dashboard is protected.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED", "verify": [1]}, + "verify-socket": {"text": "Directory/socket permissions, outsider denial, reboot persistence and container numeric IDs remain reasoned; no socket or Flower process ran.", "components": ["flower", "linux"], "sources": ["flower:s0fe6c77e9c9d", "linux:s31f41c54750c", "linux:s08902025e5f9", "linux:sa0d3793fabb7"], "status": "REASONED", "verify": [1]}, + "flower-stat-run": {"text": "Tmpfs directory-only stat distinguished 0755 and 0750; the 0750 directory initially had only base ACL entries.", "components": ["linux"], "sources": ["linux:sa0d3793fabb7", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "`stat -c '%a %U:%G %u:%g %n'` printed `755` for a directory created with mode `0755` and `750` for one created with `0750`; `getfacl -p` on the `0750` directory listed only `user::rwx`, `group::r-x` and `other::---`."}, + "flower-acl-run": {"text": "A named nobody search ACL remained invisible in stat mode 750; ls and getfacl exposed the entry.", "components": ["linux"], "sources": ["linux:s31f41c54750c", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "After `setfacl -m u:nobody:--x` on the `0750` directory, `stat` still printed `750`. Only the trailing `+` in `ls -ld` and getfacl's `user:nobody:--x` line showed the named entry."}, + "flower-mask-run": {"text": "Changing the ACL mask hid named search permission; chmod 0750 restored its reported effectiveness. Access as nobody was not tested.", "components": ["linux"], "sources": ["linux:s31f41c54750c", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "After `setfacl -m m::r--`, getfacl printed `user:nobody:--x` with `#effective:---`. While masked, `stat` printed `740`."}, + "verify-argo": {"text": "Anonymous workflow JSON is exposed when server RBAC permits; missing tokens should yield 401 without server mode. A 403 alone is inconclusive; pair authorized 200 and inspect auth modes.", "components": ["argo-three", "argo-four", "argo-docs"], "sources": ["argo-three:s49cdaf0691c0", "argo-four:s4613c15c72af", "argo-docs:s96145eacf345", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c"], "status": "REASONED", "verify": [1]}, + "verify-origin": {"text": "Any HTTP response from the origin at an untrusted vantage proves publication; use the same responding allowed origin as control and treat connection errors/timeouts as inconclusive.", "components": ["airflow", "argo-docs"], "sources": ["airflow:s1d0b86fe4334", "argo-docs:s6c8e78288632"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/pocketbase.md b/pocketbase.md index cc6b5fad2..89af0dc7c 100644 --- a/pocketbase.md +++ b/pocketbase.md @@ -1,300 +1,25 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-19", - "documentation_checked": "2026-09", - "body_sha256": "56cf568348e9a9e7890afd9133b526447b70bf593b07da5b93bedb63ce116e85", - "components": { - "pb": { - "name": "PocketBase", - "basis": "v0.40.4", - "sources": { - "saf684a26056b": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/installer.go", - "s4b155ef30262": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/cmd/serve.go", - "sb2129128b86c": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/serve.go", - "s5b66ca69bf6d": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/middlewares.go", - "s04ae861b5709": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/record_query.go", - "s69aa223f52e4": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/record_crud.go", - "sce114368c2ad": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/collection_model_auth_options.go", - "s03dd7dd84c2a": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/settings_model.go", - "seb56d3763223": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/base.go", - "sa5e571f8d473": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/file.go", - "sfd2f779e8e11": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/realtime.go", - "s31a5adccd695": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/backup.go", - "se74188862a55": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/backup_create.go#L36-L60", - "sf6ffdca4dd39": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/middlewares_rate_limit.go", - "s1b04c2cd16ec": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L234-L239", - "sb63f49346abe": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L73-L79", - "sd700e81880f2": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L129-L139", - "s54430c83a38a": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L165-L173", - "s2c8f78f2d5ac": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/tools/osutils/run.go#L10-L19", - "s5b0dd2fd7a6a": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/base.go#L1277-L1286", - "sf7c2fb5e78e7": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/base.go#L1489-L1499" - } - }, - "appwrite": { - "name": "Appwrite", - "basis": "2.2.0", - "sources": { - "s9f5bd3b70f14": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/config/variables.php", - "sadb8c6e7b00a": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/.env", - "s2bb21218c5ee": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/api/account.php", - "s193bfa5c6e22": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/shared/api.php", - "s427bab3733b6": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/general.php", - "se9480e5807b3": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Create.php", - "sf916851a0b10": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php#L82-L84", - "s5b4b3eb2271f": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L129-L134", - "s93647e16b966": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L211-L212", - "sda698c40fc34": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L237-L287", - "s655a0abb9ef0": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php", - "s4ab95ecbf6b5": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/init/constants.php", - "s8e5272615f2e": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/docker-compose.yml", - "s7ba6f9c71972": "https://github.com/appwrite/appwrite/blob/2.2.0/composer.lock#L3461-L3471", - "s5396e119c026": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php#L80-L95", - "s82988de645fa": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php#L103-L120", - "s224cc9d8a387": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Utopia/Database/Documents/User.php#L75-L109", - "sc55465cf878d": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L144-L161", - "sed1bf9453ecb": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L312-L339", - "s8784d02c82c9": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L357-L361", - "sf60a2b283b80": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L503-L507", - "s1fe0e6185518": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php#L221-L227" - } - }, - "pb-docs": { - "name": "PocketBase documentation (rolling)", - "basis": "unknown", - "sources": { - "s9029ab974cf1": "https://pocketbase.io/docs/going-to-production/", - "saedbdfbdfc48": "https://pocketbase.io/docs/api-rules-and-filters/", - "s773e441dd51c": "https://pocketbase.io/docs/authentication/", - "sf711b6345643": "https://pocketbase.io/docs/files-handling/", - "sb850a71fad4d": "https://pocketbase.io/docs/api-realtime/" - } - }, - "appwrite-docs": { - "name": "Appwrite documentation (rolling)", - "basis": "unknown", - "sources": { - "s4a965bdeeb98": "https://appwrite.io/docs/advanced/self-hosting/production/security", - "saa37bb5d4d7e": "https://appwrite.io/docs/partners/project/api-keys", - "sa0bd132f7968": "https://appwrite.io/docs/advanced/security/permissions", - "s58ad28816c5d": "https://appwrite.io/docs/products/databases/tablesdb/legacy/permissions", - "s2bf9a4746d28": "https://appwrite.io/docs/advanced/self-hosting/configuration/environment-variables", - "s000d05106450": "https://appwrite.io/docs/advanced/security/rate-limits", - "s38a1efa0c7d2": "https://appwrite.io/docs/advanced/self-hosting/configuration/email", - "s256ef73f7b85": "https://appwrite.io/docs/advanced/self-hosting/production/backups", - "s221c6aaef8df": "https://appwrite.io/docs/advanced/security/mfa" - } - }, - "curl": { - "name": "curl", - "basis": "curl-8_14_1", - "sources": { - "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", - "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md", - "s82a34ee18632": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/config.md" - } - }, - "utopia-php-database": { - "name": "utopia-php/database", - "basis": "7.3.9", - "sources": { - "s847e0ded0300": "https://github.com/utopia-php/database/blob/54364c687d6ddb3d976d7130b9001c0b29276933/src/Database/Helpers/Permission.php#L151-L155" - } - } - }, - "claims": { - "pb-bootstrap": {"text": "Privately bootstrap through the logged installer URL; superuser create EMAIL PASS exposes passwords in argv. Ordinary registration does not grant superuser rights.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:saf684a26056b"], "status": "REASONED"}, - "pb-installer": {"text": "Installer system-superuser token lasts about 30 minutes; protect its URL and startup logs as credentials.", "components": ["pb"], "sources": ["pb:saf684a26056b"], "status": "REASONED"}, - "pb-ips": {"text": "Superuser IP allowlists are available from v0.38.0; superuserIPs defaults empty and restricts authenticated requests, not /_/ static assets.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s5b66ca69bf6d"], "status": "REASONED"}, - "pb-tls": {"text": "serve with a domain provides native HTTPS/ACME; alternatively terminate TLS at a reverse proxy.", "components": ["pb"], "sources": ["pb:sb2129128b86c"], "status": "REASONED"}, - "pb-bind": {"text": "Without a domain serve defaults to 127.0.0.1:8090; with a domain it uses 0.0.0.0:80/443. Behind a local proxy omit the domain and set --http loopback.", "components": ["pb"], "sources": ["pb:s4b155ef30262"], "status": "REASONED"}, - "pb-origins": {"text": "--origins defaults * and accepts comma-separated allowed origins; CORS does not authorize records.", "components": ["pb"], "sources": ["pb:s4b155ef30262"], "status": "REASONED"}, - "pb-proxy": {"text": "Trust only headers the proxy overwrites, review useLeftmostIP ordering and block direct backend access to protect IP/rate controls.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a"], "status": "REASONED"}, - "pb-dev": {"text": "--dev adds diagnostics including SQL on stderr, not an auth bypass; its configured default comes from New(): true if os.Args[0] starts with a nonempty os.TempDir() or cacheDir() prefix, false otherwise.", "components": ["pb"], "sources": ["pb:seb56d3763223", "pb:s1b04c2cd16ec", "pb:sb63f49346abe", "pb:s2c8f78f2d5ac", "pb:sd700e81880f2", "pb:s5b0dd2fd7a6a", "pb:sf7c2fb5e78e7"], "status": "REASONED"}, - "pb-cli": {"text": "In the pinned CLI wrapper, Start() registers the serve and superuser commands on the root command and executes it.", "components": ["pb"], "sources": ["pb:s54430c83a38a"], "status": "REASONED"}, - "pb-rules": {"text": "Each list/view/create/update/delete rule defaults null (superuser-only); empty opens to guests and nonempty filters. Superusers bypass rules.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:saedbdfbdfc48", "pb:s04ae861b5709"], "status": "REASONED"}, - "pb-manage": {"text": "Top-level manageRule defaults null and permits privileged auth-record changes alongside create/update rules; its validator rejects empty-string rules.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, - "pb-files": {"text": "File fields default unprotected despite locked record rules; Protected plus an authorized viewRule gates downloads, while a public viewRule still permits public access.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sf711b6345643", "pb:sa5e571f8d473"], "status": "REASONED"}, - "pb-file-token": {"text": "Short-lived file tokens provide identity context, not file authorization; viewRule decides. Protect token-bearing file URLs.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sf711b6345643", "pb:sa5e571f8d473"], "status": "REASONED"}, - "pb-realtime": {"text": "Collection subscriptions use listRule and individual-record subscriptions viewRule; SSE/subscription success does not prove event authorization.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sb850a71fad4d", "pb:sfd2f779e8e11"], "status": "REASONED"}, - "pb-password": {"text": "Auth initialization enables password authentication with email as the identity field.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, - "pb-oauth": {"text": "OAuth2 defaults disabled and is unsupported for _superusers.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, - "pb-mfa-default": {"text": "MFA defaults disabled with duration 600 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, - "pb-otp-default": {"text": "OTP defaults disabled with duration 180 seconds and length 8.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, - "pb-alerts": {"text": "Authentication alerts default enabled; retain alerts and provide working email.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, - "pb-auth-token": {"text": "Auth token duration defaults to 432000 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, - "pb-file-duration": {"text": "File token duration defaults to 180 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, - "pb-auth-rule": {"text": "authRule defaults empty; verified-only collections can require verified = true after email verification works.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, - "pb-mfa": {"text": "For superusers retain password auth and enable both OTP and MFA; empty mfa.rule applies to everyone. OTP alone is not a second factor.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, - "pb-logout": {"text": "Stateless authentication means clearing pb.authStore removes only the local copy, not a stolen token.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s773e441dd51c", "pb:s04ae861b5709"], "status": "REASONED"}, - "pb-encryption": {"text": "Settings default to plaintext JSON; select a 32-character environment secret with --encryptionEnv. This encrypts settings, not the whole database or files.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a"], "status": "REASONED"}, - "pb-backups": {"text": "Backups default local with empty cron (automatic backups off); archives contain main and logs databases plus remaining pb_data, excluding the backups directory and tracked exclusions; uploaded collection files on S3 require manual backup. Schedule/retain deliberately and protect archives.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a", "pb:se74188862a55"], "status": "REASONED"}, - "pb-backup-auth": {"text": "Backup downloads require a superuser file token and obey superuser IP restrictions; ordinary auth/file tokens do not suffice.", "components": ["pb"], "sources": ["pb:s31a5adccd695"], "status": "REASONED"}, - "pb-limiter": {"text": "Native limits exist from v0.23.0 but default disabled at v0.40.4; enable rateLimits.enabled.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a"], "status": "REASONED"}, - "pb-limit-rules": {"text": "Seeded limits are *:auth 2/3s, *:create 20/5s, /api/batch 3/1s and /api/ 300/10s.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a"], "status": "REASONED"}, - "pb-limit-bypass": {"text": "Superusers and rateLimits.excludedIPs bypass limits; test ordinary non-excluded clients.", "components": ["pb"], "sources": ["pb:sf6ffdca4dd39"], "status": "REASONED"}, - "pb-smtp": {"text": "smtp.enabled and smtp.tls default false; disabled SMTP falls back to sendmail. Configure protected transport and test actual delivery.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a", "pb:seb56d3763223"], "status": "REASONED"}, - "appwrite-https": {"text": "API FORCE_HTTPS and function/site ROUTER_FORCE_HTTPS default disabled at 2.2.0; explicitly enable both despite conflicting rolling deprecation/default documentation.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s427bab3733b6", "appwrite-docs:s2bf9a4746d28"], "status": "REASONED"}, - "appwrite-hosts": {"text": "ROUTER_PROTECTION defaults disabled; configure actual domains and enable rejection of unknown hostnames.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s427bab3733b6"], "status": "REASONED"}, - "appwrite-registration": {"text": "Console root-only registration defaults enabled with empty email/IP allowlists; these restrict account creation, not dashboard reachability or login.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s2bb21218c5ee"], "status": "REASONED"}, - "appwrite-console": {"text": "Claim the first operator privately and use a separate network/access boundary for a private dashboard.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s4a965bdeeb98", "appwrite:s2bb21218c5ee"], "status": "REASONED"}, - "appwrite-mfa": {"text": "Enroll and verify operator TOTP, protect recovery codes; at 2.2.0 enabled MFA with a verified factor requires a second session factor.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s221c6aaef8df", "appwrite:s193bfa5c6e22"], "status": "REASONED"}, - "appwrite-dev-env": {"text": "Development env disables root-only registration, abuse and router protection and carries placeholders; do not inherit it for production.", "components": ["appwrite"], "sources": ["appwrite:sadb8c6e7b00a"], "status": "REASONED"}, - "appwrite-recreate": {"text": "Apply env/Compose changes with docker compose up -d from the install directory, then check effective behavior.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s4a965bdeeb98"], "status": "REASONED"}, - "appwrite-permissions": {"text": "Collection and document grants are additive according to legacy documentation; documentSecurity defaults false in Appwrite 2.2.0 and must be enabled for document grants. The pinned parameter description states the need for one permission level.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:sf916851a0b10", "appwrite-docs:s58ad28816c5d"], "status": "REASONED"}, - "appwrite-creation": {"text": "For null or omitted document permissions, the pinned aggregate helper preserves null before the handler assigns read/update/delete user grants to a nonprivileged requester with a user ID, or an empty list otherwise. API-key classification skips role validation but does not itself determine the default list. Use specific users/teams for private data.", "components": ["appwrite", "appwrite-docs", "utopia-php-database"], "sources": ["appwrite:s5b4b3eb2271f", "appwrite:s93647e16b966", "appwrite:sda698c40fc34", "appwrite-docs:sa0bd132f7968", "appwrite:s7ba6f9c71972", "utopia-php-database:s847e0ded0300"], "status": "REASONED"}, - "appwrite-read-path": {"text": "Document GET classifies the requester from authorization roles (keys; owner/developer/admin privileged), reports disabled databases/collections as not found except to those requesters, and fetches the document through the database layer; collection permissions and documentSecurity are passed to the database layer at collection creation.", "components": ["appwrite"], "sources": ["appwrite:s5396e119c026", "appwrite:s82988de645fa", "appwrite:s224cc9d8a387", "appwrite:s1fe0e6185518"], "status": "REASONED"}, - "appwrite-requester": {"text": "With an API key the request role and scopes come from the key; a standard or ephemeral project key for the same project disables the default authorization status, as do admin-mode console sessions outside console project routes, while route scope validation applies to every requester.", "components": ["appwrite"], "sources": ["appwrite:sc55465cf878d", "appwrite:sed1bf9453ecb", "appwrite:s8784d02c82c9", "appwrite:sf60a2b283b80"], "status": "REASONED"}, - "appwrite-keys": {"text": "Properly scoped server API keys bypass resource permissions but still obey operation scopes; keys.write is admin-equivalent and keys never belong in clients.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s193bfa5c6e22", "appwrite-docs:saa37bb5d4d7e"], "status": "REASONED"}, - "appwrite-encryption": {"text": "Replace _APP_OPENSSL_KEY_V1=your-secret-key before production data; preserve the exact key separately, since replacement/loss strands encrypted secrets.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, - "appwrite-backups": {"text": "Back up database, persistent storage and deployment configuration; restrict access and test restoration in a separate installation.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, - "appwrite-abuse": {"text": "_APP_OPTIONS_ABUSE defaults enabled but dev env disables it; server API-key requests are exempt, so test with ordinary clients.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:sadb8c6e7b00a", "appwrite:s193bfa5c6e22", "appwrite-docs:s000d05106450"], "status": "REASONED"}, - "appwrite-smtp": {"text": "SMTP host/port/secure/username/password default empty and empty host disables sending; configure the provider and confirm message receipt/use.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite-docs:s38a1efa0c7d2"], "status": "REASONED"}, - "appwrite-storage": {"text": "Storage defaults local, upload limit 30000000 and server antivirus disabled; enabling scanning requires configured reachable ClamAV.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14"], "status": "REASONED"}, - "appwrite-bucket": {"text": "New bucket permissions are empty and fileSecurity=false; enable per-file security deliberately, since bucket grants also permit access.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3"], "status": "REASONED"}, - "appwrite-bucket-options": {"text": "Bucket encryption and antivirus default true; bucket antivirus does not enable the server scanner.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3"], "status": "REASONED"}, - "appwrite-size": {"text": "Encryption/scanning skip files above 20000000 bytes; cap maximumFileSize at that threshold when either is mandatory and test the boundary.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3", "appwrite:s4ab95ecbf6b5"], "status": "REASONED"}, - "appwrite-functions": {"text": "Function execute roles and execution-key scopes default empty; grant narrowly. Disabled functions still admit authorized Server SDK API keys.", "components": ["appwrite"], "sources": ["appwrite:s655a0abb9ef0"], "status": "REASONED"}, - "appwrite-executor": {"text": "Keep executor/orchestrator private, protect Docker-socket authority and replace _APP_EXECUTOR_SECRET; container execution is not demonstrated hostile-tenant isolation.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s8e5272615f2e", "appwrite-docs:s2bf9a4746d28"], "status": "REASONED"}, - "verify-pb-records": {"text": "Public rules allow guest reads; locked rules deny with 403, unsatisfied list filters can return empty 200 and view rules 404. Retain owner and separate superuser controls.", "components": ["pb-docs", "curl"], "sources": ["pb-docs:saedbdfbdfc48", "curl:s5e1383feef7d", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, - "verify-appwrite-records": {"text": "Broad collection grants defeat document restrictions; removing them must deny unrelated users while document grantees succeed. Compare client/server creation defaults.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s58ad28816c5d", "appwrite-docs:sa0bd132f7968"], "status": "REASONED", "verify": [1]}, - "verify-appwrite-keys": {"text": "A correctly scoped server key reads despite empty permissions, while a key without scope fails; neither substitutes for an ordinary-user control.", "components": ["appwrite"], "sources": ["appwrite:s193bfa5c6e22"], "status": "REASONED", "verify": [1]}, - "verify-registration": {"text": "On disposable Appwrite fixtures compare open signup 201 with root-only account-limit failure and independent email/IP restrictions; retain login/invitation controls.", "components": ["appwrite"], "sources": ["appwrite:s2bb21218c5ee"], "status": "REASONED", "verify": [2]}, - "verify-pb-bootstrap": {"text": "Guest and ordinary-user _superusers record creation must fail while a valid installer token creates the private operator; static dashboard assets prove no admin access.", "components": ["pb"], "sources": ["pb:saf684a26056b", "pb:s69aa223f52e4"], "status": "REASONED"}, - "verify-appwrite-mfa": {"text": "Console account GET succeeds without MFA, fails with user_more_factors_required for a fresh password-only MFA session, and succeeds after TOTP completion.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s221c6aaef8df", "appwrite:s193bfa5c6e22"], "status": "REASONED"}, - "verify-pb-writes": {"text": "Compare intended/unauthorized POST/PATCH/DELETE and privileged auth-record management; locked manageRule grants no management and empty-string configuration must fail.", "components": ["pb"], "sources": ["pb:s69aa223f52e4", "pb:sce114368c2ad"], "status": "REASONED"}, - "verify-pb-files": {"text": "Before protection unsigned file GET works; Protected plus private viewRule rejects guests/unrelated-user file tokens with 404 while permitted-user bytes still arrive.", "components": ["pb", "curl"], "sources": ["pb:sa5e571f8d473", "curl:s82a34ee18632"], "status": "REASONED"}, - "verify-pb-realtime": {"text": "Mutate a fixture while comparing collection and record subscriptions; deny unauthorized event delivery, retaining authorized delivery. PB_CONNECT/acknowledgment alone is insufficient.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sb850a71fad4d", "pb:sfd2f779e8e11"], "status": "REASONED"}, - "verify-pb-mfa": {"text": "Compare password-only completion with password-plus-OTP MFA and verified/unverified authRule logins; clearing authStore must not revoke a retained valid token.", "components": ["pb-docs"], "sources": ["pb-docs:s773e441dd51c"], "status": "REASONED"}, - "verify-pb-ips": {"text": "Compare allowed/excluded superuser sources and forged forwarded headers; excluded sources must get 403 and direct backend access must fail with a working proxy control.", "components": ["pb"], "sources": ["pb:s5b66ca69bf6d", "pb:s03dd7dd84c2a"], "status": "REASONED"}, - "verify-transport": {"text": "Compare HTTP/HTTPS and certificates, Appwrite API/function/site domains and configured/unknown Host; compare browser origins separately from record authorization.", "components": ["pb", "appwrite"], "sources": ["pb:s4b155ef30262", "appwrite:s427bab3733b6"], "status": "REASONED"}, - "verify-abuse": {"text": "Cross each configured threshold with ordinary non-excluded clients and compare disabled/enabled controls; PocketBase limiting yields 429 with later successful recovery.", "components": ["pb", "appwrite-docs"], "sources": ["pb:sf6ffdca4dd39", "appwrite-docs:s000d05106450"], "status": "REASONED"}, - "verify-email": {"text": "Request OTP, verification and recovery messages and confirm receipt and use; request success alone is not delivery evidence.", "components": ["pb", "appwrite-docs"], "sources": ["pb:seb56d3763223", "appwrite-docs:s38a1efa0c7d2"], "status": "REASONED"}, - "verify-pb-recovery": {"text": "Compare plaintext/encrypted persisted settings, restart with retained key and restore local/S3 data; only permitted superuser file tokens may download backups.", "components": ["pb", "pb-docs"], "sources": ["pb:s03dd7dd84c2a", "pb:s31a5adccd695", "pb-docs:s9029ab974cf1"], "status": "REASONED"}, - "verify-appwrite-recovery": {"text": "Restore encrypted fixtures with the original key; a separate wrong-key fixture must not recover encrypted values.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, - "verify-uploads": {"text": "Compare permitted/unrelated uploads and size-boundary fixtures; mandatory processing requires rejecting over-limit files and confirming accepted-file encryption/scanning.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3", "appwrite:s4ab95ecbf6b5"], "status": "REASONED"}, - "verify-functions": {"text": "Compare intended execute roles, unrelated users and privileged server keys, including disabled functions; confirm executor/orchestrator privacy from a second host.", "components": ["appwrite"], "sources": ["appwrite:s655a0abb9ef0", "appwrite:s8e5272615f2e"], "status": "REASONED"}, - "verify-bundle": {"text": "Search filenames using protected exact-secret patterns with a disposable marker control; client bundles must contain neither superuser tokens nor Appwrite API keys, and encoded/split forms can evade scanning.", "components": ["pb-docs", "appwrite-docs"], "sources": ["pb-docs:s773e441dd51c", "appwrite-docs:saa37bb5d4d7e"], "status": "REASONED", "verify": [3]}, - "local-guards": {"text": "Recorded guard tests rejected placeholders and omitted/shortened arguments; this demonstrates shell behavior only, not service access or deployed-bundle scanning.", "components": ["pb-docs", "appwrite-docs"], "sources": ["pb-docs:saedbdfbdfc48", "appwrite-docs:sa0bd132f7968", "appwrite-docs:saa37bb5d4d7e"], "status": "DEMONSTRATED", "evidence": "Local guard tests refused embedded `REPLACE_WITH_` placeholders, `example.com`, angle brackets, empty arguments, and omitted or shortened `set --` lines."}, - "local-syntax": {"text": "All three bash fences have recorded local lint/syntax success, not live service evidence; ShellCheck version is recorded only in the body.", "components": ["appwrite", "pb-docs", "appwrite-docs"], "sources": ["pb-docs:saedbdfbdfc48", "appwrite:s2bb21218c5ee", "appwrite-docs:saa37bb5d4d7e"], "status": "DEMONSTRATED", "evidence": "All three bash blocks passed ShellCheck 0.11.0 and `bash -n` during authoring."}, - "source-limits": {"text": "The PocketBase CLI wrapper, --dev default, and Appwrite requester classification and document-read call sites are now cited; how utopia-php/database matches roles against collection/document permissions (including documentSecurity) and what the disabled default authorization status bypasses remain untraced in that unfetched dependency.", "components": ["appwrite-docs", "pb-docs", "appwrite"], "sources": ["appwrite-docs:s58ad28816c5d", "pb-docs:s9029ab974cf1", "appwrite:s7ba6f9c71972"], "status": "REASONED"} - } -} ---- # Self-hosted backends: PocketBase and Appwrite - -**Version basis** +Written against PocketBase v0.40.4, Appwrite 2.2.0, utopia-php/database 7.3.9, and curl curl-8_14_1; 2 further sources carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 75 claims: 2 demonstrated by a check that ran and 73 reasoned from cited sources. -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +Like Firebase and Supabase ([firebase-supabase.md](firebase-supabase.md)), these backends hand a +public API endpoint to your client code; the collection or resource rules you write gate access +to your data. Files and privileged server credentials need separate attention. Both also ship +an admin console, so complete bootstrap privately before exposing the instance. Appwrite's first +console registrant becomes its initial operator; PocketBase's installer requires a privileged +token from the startup log, not merely an ordinary user's registration. -Components (recorded basis): +The source checks below target **PocketBase v0.40.4** and **Appwrite 2.2.0**, with documentation +checked in September 2026. Defaults refer to those releases unless stated otherwise. Live behavior +has not been demonstrated in this authoring environment. Some pin-specific source checks remain +incomplete; the source-work table below records those. Live comparisons are REASONED from the +cited vendor documentation and the pinned sources that were available. -- pb: PocketBase (v0.40.4) -- appwrite: Appwrite (2.2.0) -- pb-docs: PocketBase documentation (rolling) (unknown) -- appwrite-docs: Appwrite documentation (rolling) (unknown) -- curl: curl (curl-8_14_1) -- utopia-php-database: utopia-php/database (7.3.9) +## PocketBase -Claims (full source mapping in the front matter): - -- pb-bootstrap (REASONED; pb, pb-docs): Privately bootstrap through the logged installer URL; superuser create EMAIL PASS exposes passwords in argv. Ordinary registration does not grant superuser rights. -- pb-installer (REASONED; pb): Installer system-superuser token lasts about 30 minutes; protect its URL and startup logs as credentials. -- pb-ips (REASONED; pb, pb-docs): Superuser IP allowlists are available from v0.38.0; superuserIPs defaults empty and restricts authenticated requests, not /_/ static assets. -- pb-tls (REASONED; pb): serve with a domain provides native HTTPS/ACME; alternatively terminate TLS at a reverse proxy. -- pb-bind (REASONED; pb): Without a domain serve defaults to 127.0.0.1:8090; with a domain it uses 0.0.0.0:80/443. Behind a local proxy omit the domain and set --http loopback. -- pb-origins (REASONED; pb): --origins defaults * and accepts comma-separated allowed origins; CORS does not authorize records. -- pb-proxy (REASONED; pb): Trust only headers the proxy overwrites, review useLeftmostIP ordering and block direct backend access to protect IP/rate controls. -- pb-dev (REASONED; pb): --dev adds diagnostics including SQL on stderr, not an auth bypass; its configured default comes from New(): true if os.Args[0] starts with a nonempty os.TempDir() or cacheDir() prefix, false otherwise. -- pb-cli (REASONED; pb): In the pinned CLI wrapper, Start() registers the serve and superuser commands on the root command and executes it. -- pb-rules (REASONED; pb, pb-docs): Each list/view/create/update/delete rule defaults null (superuser-only); empty opens to guests and nonempty filters. Superusers bypass rules. -- pb-manage (REASONED; pb): Top-level manageRule defaults null and permits privileged auth-record changes alongside create/update rules; its validator rejects empty-string rules. -- pb-files (REASONED; pb, pb-docs): File fields default unprotected despite locked record rules; Protected plus an authorized viewRule gates downloads, while a public viewRule still permits public access. -- pb-file-token (REASONED; pb, pb-docs): Short-lived file tokens provide identity context, not file authorization; viewRule decides. Protect token-bearing file URLs. -- pb-realtime (REASONED; pb, pb-docs): Collection subscriptions use listRule and individual-record subscriptions viewRule; SSE/subscription success does not prove event authorization. -- pb-password (REASONED; pb): Auth initialization enables password authentication with email as the identity field. -- pb-oauth (REASONED; pb, pb-docs): OAuth2 defaults disabled and is unsupported for _superusers. -- pb-mfa-default (REASONED; pb): MFA defaults disabled with duration 600 seconds. -- pb-otp-default (REASONED; pb): OTP defaults disabled with duration 180 seconds and length 8. -- pb-alerts (REASONED; pb): Authentication alerts default enabled; retain alerts and provide working email. -- pb-auth-token (REASONED; pb): Auth token duration defaults to 432000 seconds. -- pb-file-duration (REASONED; pb): File token duration defaults to 180 seconds. -- pb-auth-rule (REASONED; pb, pb-docs): authRule defaults empty; verified-only collections can require verified = true after email verification works. -- pb-mfa (REASONED; pb, pb-docs): For superusers retain password auth and enable both OTP and MFA; empty mfa.rule applies to everyone. OTP alone is not a second factor. -- pb-logout (REASONED; pb, pb-docs): Stateless authentication means clearing pb.authStore removes only the local copy, not a stolen token. -- pb-encryption (REASONED; pb, pb-docs): Settings default to plaintext JSON; select a 32-character environment secret with --encryptionEnv. This encrypts settings, not the whole database or files. -- pb-backups (REASONED; pb, pb-docs): Backups default local with empty cron (automatic backups off); archives contain main and logs databases plus remaining pb_data, excluding the backups directory and tracked exclusions; uploaded collection files on S3 require manual backup. Schedule/retain deliberately and protect archives. -- pb-backup-auth (REASONED; pb): Backup downloads require a superuser file token and obey superuser IP restrictions; ordinary auth/file tokens do not suffice. -- pb-limiter (REASONED; pb, pb-docs): Native limits exist from v0.23.0 but default disabled at v0.40.4; enable rateLimits.enabled. -- pb-limit-rules (REASONED; pb): Seeded limits are *:auth 2/3s, *:create 20/5s, /api/batch 3/1s and /api/ 300/10s. -- pb-limit-bypass (REASONED; pb): Superusers and rateLimits.excludedIPs bypass limits; test ordinary non-excluded clients. -- pb-smtp (REASONED; pb): smtp.enabled and smtp.tls default false; disabled SMTP falls back to sendmail. Configure protected transport and test actual delivery. -- appwrite-https (REASONED; appwrite, appwrite-docs): API FORCE_HTTPS and function/site ROUTER_FORCE_HTTPS default disabled at 2.2.0; explicitly enable both despite conflicting rolling deprecation/default documentation. -- appwrite-hosts (REASONED; appwrite): ROUTER_PROTECTION defaults disabled; configure actual domains and enable rejection of unknown hostnames. -- appwrite-registration (REASONED; appwrite): Console root-only registration defaults enabled with empty email/IP allowlists; these restrict account creation, not dashboard reachability or login. -- appwrite-console (REASONED; appwrite, appwrite-docs): Claim the first operator privately and use a separate network/access boundary for a private dashboard. -- appwrite-mfa (REASONED; appwrite, appwrite-docs): Enroll and verify operator TOTP, protect recovery codes; at 2.2.0 enabled MFA with a verified factor requires a second session factor. -- appwrite-dev-env (REASONED; appwrite): Development env disables root-only registration, abuse and router protection and carries placeholders; do not inherit it for production. -- appwrite-recreate (REASONED; appwrite-docs): Apply env/Compose changes with docker compose up -d from the install directory, then check effective behavior. -- appwrite-permissions (REASONED; appwrite, appwrite-docs): Collection and document grants are additive according to legacy documentation; documentSecurity defaults false in Appwrite 2.2.0 and must be enabled for document grants. The pinned parameter description states the need for one permission level. -- appwrite-creation (REASONED; appwrite, appwrite-docs, utopia-php-database): For null or omitted document permissions, the pinned aggregate helper preserves null before the handler assigns read/update/delete user grants to a nonprivileged requester with a user ID, or an empty list otherwise. API-key classification skips role validation but does not itself determine the default list. Use specific users/teams for private data. -- appwrite-read-path (REASONED; appwrite): Document GET classifies the requester from authorization roles (keys; owner/developer/admin privileged), reports disabled databases/collections as not found except to those requesters, and fetches the document through the database layer; collection permissions and documentSecurity are passed to the database layer at collection creation. -- appwrite-requester (REASONED; appwrite): With an API key the request role and scopes come from the key; a standard or ephemeral project key for the same project disables the default authorization status, as do admin-mode console sessions outside console project routes, while route scope validation applies to every requester. -- appwrite-keys (REASONED; appwrite, appwrite-docs): Properly scoped server API keys bypass resource permissions but still obey operation scopes; keys.write is admin-equivalent and keys never belong in clients. -- appwrite-encryption (REASONED; appwrite, appwrite-docs): Replace _APP_OPENSSL_KEY_V1=your-secret-key before production data; preserve the exact key separately, since replacement/loss strands encrypted secrets. -- appwrite-backups (REASONED; appwrite-docs): Back up database, persistent storage and deployment configuration; restrict access and test restoration in a separate installation. -- appwrite-abuse (REASONED; appwrite, appwrite-docs): _APP_OPTIONS_ABUSE defaults enabled but dev env disables it; server API-key requests are exempt, so test with ordinary clients. -- appwrite-smtp (REASONED; appwrite, appwrite-docs): SMTP host/port/secure/username/password default empty and empty host disables sending; configure the provider and confirm message receipt/use. -- appwrite-storage (REASONED; appwrite): Storage defaults local, upload limit 30000000 and server antivirus disabled; enabling scanning requires configured reachable ClamAV. -- appwrite-bucket (REASONED; appwrite): New bucket permissions are empty and fileSecurity=false; enable per-file security deliberately, since bucket grants also permit access. -- appwrite-bucket-options (REASONED; appwrite): Bucket encryption and antivirus default true; bucket antivirus does not enable the server scanner. -- appwrite-size (REASONED; appwrite): Encryption/scanning skip files above 20000000 bytes; cap maximumFileSize at that threshold when either is mandatory and test the boundary. -- appwrite-functions (REASONED; appwrite): Function execute roles and execution-key scopes default empty; grant narrowly. Disabled functions still admit authorized Server SDK API keys. -- appwrite-executor (REASONED; appwrite, appwrite-docs): Keep executor/orchestrator private, protect Docker-socket authority and replace _APP_EXECUTOR_SECRET; container execution is not demonstrated hostile-tenant isolation. -- verify-pb-records (REASONED; pb-docs, curl): Public rules allow guest reads; locked rules deny with 403, unsatisfied list filters can return empty 200 and view rules 404. Retain owner and separate superuser controls. -- verify-appwrite-records (REASONED; appwrite-docs): Broad collection grants defeat document restrictions; removing them must deny unrelated users while document grantees succeed. Compare client/server creation defaults. -- verify-appwrite-keys (REASONED; appwrite): A correctly scoped server key reads despite empty permissions, while a key without scope fails; neither substitutes for an ordinary-user control. -- verify-registration (REASONED; appwrite): On disposable Appwrite fixtures compare open signup 201 with root-only account-limit failure and independent email/IP restrictions; retain login/invitation controls. -- verify-pb-bootstrap (REASONED; pb): Guest and ordinary-user _superusers record creation must fail while a valid installer token creates the private operator; static dashboard assets prove no admin access. -- verify-appwrite-mfa (REASONED; appwrite, appwrite-docs): Console account GET succeeds without MFA, fails with user_more_factors_required for a fresh password-only MFA session, and succeeds after TOTP completion. -- verify-pb-writes (REASONED; pb): Compare intended/unauthorized POST/PATCH/DELETE and privileged auth-record management; locked manageRule grants no management and empty-string configuration must fail. -- verify-pb-files (REASONED; pb, curl): Before protection unsigned file GET works; Protected plus private viewRule rejects guests/unrelated-user file tokens with 404 while permitted-user bytes still arrive. -- verify-pb-realtime (REASONED; pb, pb-docs): Mutate a fixture while comparing collection and record subscriptions; deny unauthorized event delivery, retaining authorized delivery. PB_CONNECT/acknowledgment alone is insufficient. -- verify-pb-mfa (REASONED; pb-docs): Compare password-only completion with password-plus-OTP MFA and verified/unverified authRule logins; clearing authStore must not revoke a retained valid token. -- verify-pb-ips (REASONED; pb): Compare allowed/excluded superuser sources and forged forwarded headers; excluded sources must get 403 and direct backend access must fail with a working proxy control. -- verify-transport (REASONED; pb, appwrite): Compare HTTP/HTTPS and certificates, Appwrite API/function/site domains and configured/unknown Host; compare browser origins separately from record authorization. -- verify-abuse (REASONED; pb, appwrite-docs): Cross each configured threshold with ordinary non-excluded clients and compare disabled/enabled controls; PocketBase limiting yields 429 with later successful recovery. -- verify-email (REASONED; pb, appwrite-docs): Request OTP, verification and recovery messages and confirm receipt and use; request success alone is not delivery evidence. -- verify-pb-recovery (REASONED; pb, pb-docs): Compare plaintext/encrypted persisted settings, restart with retained key and restore local/S3 data; only permitted superuser file tokens may download backups. -- verify-appwrite-recovery (REASONED; appwrite-docs): Restore encrypted fixtures with the original key; a separate wrong-key fixture must not recover encrypted values. -- verify-uploads (REASONED; appwrite): Compare permitted/unrelated uploads and size-boundary fixtures; mandatory processing requires rejecting over-limit files and confirming accepted-file encryption/scanning. -- verify-functions (REASONED; appwrite): Compare intended execute roles, unrelated users and privileged server keys, including disabled functions; confirm executor/orchestrator privacy from a second host. -- verify-bundle (REASONED; pb-docs, appwrite-docs): Search filenames using protected exact-secret patterns with a disposable marker control; client bundles must contain neither superuser tokens nor Appwrite API keys, and encoded/split forms can evade scanning. -- local-guards (DEMONSTRATED; pb-docs, appwrite-docs): Recorded guard tests rejected placeholders and omitted/shortened arguments; this demonstrates shell behavior only, not service access or deployed-bundle scanning. -- local-syntax (DEMONSTRATED; appwrite, pb-docs, appwrite-docs): All three bash fences have recorded local lint/syntax success, not live service evidence; ShellCheck version is recorded only in the body. -- source-limits (REASONED; appwrite-docs, pb-docs, appwrite): The PocketBase CLI wrapper, --dev default, and Appwrite requester classification and document-read call sites are now cited; how utopia-php/database matches roles against collection/document permissions (including documentSecurity) and what the disabled default authorization status bypasses remain untraced in that unfetched dependency. - - -Like Firebase and Supabase ([firebase-supabase.md](firebase-supabase.md)), these backends hand a -public API endpoint to your client code; the collection or resource rules you write gate access -to your data. Files and privileged server credentials need separate attention. Both also ship -an admin console, so complete bootstrap privately before exposing the instance. Appwrite's first -console registrant becomes its initial operator; PocketBase's installer requires a privileged -token from the startup log, not merely an ordinary user's registration. - -The source checks below target **PocketBase v0.40.4** and **Appwrite 2.2.0**, with documentation -checked in September 2026. Defaults refer to those releases unless stated otherwise. Live behavior -has not been demonstrated in this authoring environment. Some pin-specific source checks remain -incomplete; the source-work table below records those. Live comparisons are REASONED from the -cited vendor documentation and the pinned sources that were available. - -## PocketBase - -### 1. Bootstrap privately and protect the operator +### 1. Bootstrap privately and protect the operator Create the superuser before opening access. The documented console syntax is `./pocketbase superuser create EMAIL PASS`; the alternative is the web-based installer linked from @@ -966,3 +691,304 @@ service result or deployed-bundle scan is claimed. - [curl curl-8_14_1 write-out fields and version requirements (checked October 2026)](https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79). - [curl curl-8_14_1 headers from stdin (checked October 2026)](https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md). - [curl curl-8_14_1 configuration from stdin (checked October 2026)](https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/config.md). + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/pocketbase.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- pb: PocketBase (v0.40.4) +- appwrite: Appwrite (2.2.0) +- pb-docs: PocketBase documentation (rolling) (unknown) +- appwrite-docs: Appwrite documentation (rolling) (unknown) +- utopia-php-database: utopia-php/database (7.3.9) +- curl: curl (curl-8_14_1) + +Claims (full source mapping in the JSON block below): + +- pb-bootstrap (REASONED; pb, pb-docs): Privately bootstrap through the logged installer URL; superuser create EMAIL PASS exposes passwords in argv. Ordinary registration does not grant superuser rights. +- pb-installer (REASONED; pb): Installer system-superuser token lasts about 30 minutes; protect its URL and startup logs as credentials. +- pb-ips (REASONED; pb, pb-docs): Superuser IP allowlists are available from v0.38.0; superuserIPs defaults empty and restricts authenticated requests, not /_/ static assets. +- pb-tls (REASONED; pb): serve with a domain provides native HTTPS/ACME; alternatively terminate TLS at a reverse proxy. +- pb-bind (REASONED; pb): Without a domain serve defaults to 127.0.0.1:8090; with a domain it uses 0.0.0.0:80/443. Behind a local proxy omit the domain and set --http loopback. +- pb-origins (REASONED; pb): --origins defaults * and accepts comma-separated allowed origins; CORS does not authorize records. +- pb-proxy (REASONED; pb): Trust only headers the proxy overwrites, review useLeftmostIP ordering and block direct backend access to protect IP/rate controls. +- pb-dev (REASONED; pb): --dev adds diagnostics including SQL on stderr, not an auth bypass; its configured default comes from New(): true if os.Args[0] starts with a nonempty os.TempDir() or cacheDir() prefix, false otherwise. +- pb-cli (REASONED; pb): In the pinned CLI wrapper, Start() registers the serve and superuser commands on the root command and executes it. +- pb-rules (REASONED; pb, pb-docs): Each list/view/create/update/delete rule defaults null (superuser-only); empty opens to guests and nonempty filters. Superusers bypass rules. +- pb-manage (REASONED; pb): Top-level manageRule defaults null and permits privileged auth-record changes alongside create/update rules; its validator rejects empty-string rules. +- pb-files (REASONED; pb, pb-docs): File fields default unprotected despite locked record rules; Protected plus an authorized viewRule gates downloads, while a public viewRule still permits public access. +- pb-file-token (REASONED; pb, pb-docs): Short-lived file tokens provide identity context, not file authorization; viewRule decides. Protect token-bearing file URLs. +- pb-realtime (REASONED; pb, pb-docs): Collection subscriptions use listRule and individual-record subscriptions viewRule; SSE/subscription success does not prove event authorization. +- pb-password (REASONED; pb): Auth initialization enables password authentication with email as the identity field. +- pb-oauth (REASONED; pb, pb-docs): OAuth2 defaults disabled and is unsupported for _superusers. +- pb-mfa-default (REASONED; pb): MFA defaults disabled with duration 600 seconds. +- pb-otp-default (REASONED; pb): OTP defaults disabled with duration 180 seconds and length 8. +- pb-alerts (REASONED; pb): Authentication alerts default enabled; retain alerts and provide working email. +- pb-auth-token (REASONED; pb): Auth token duration defaults to 432000 seconds. +- pb-file-duration (REASONED; pb): File token duration defaults to 180 seconds. +- pb-auth-rule (REASONED; pb, pb-docs): authRule defaults empty; verified-only collections can require verified = true after email verification works. +- pb-mfa (REASONED; pb, pb-docs): For superusers retain password auth and enable both OTP and MFA; empty mfa.rule applies to everyone. OTP alone is not a second factor. +- pb-logout (REASONED; pb, pb-docs): Stateless authentication means clearing pb.authStore removes only the local copy, not a stolen token. +- pb-encryption (REASONED; pb, pb-docs): Settings default to plaintext JSON; select a 32-character environment secret with --encryptionEnv. This encrypts settings, not the whole database or files. +- pb-backups (REASONED; pb, pb-docs): Backups default local with empty cron (automatic backups off); archives contain main and logs databases plus remaining pb_data, excluding the backups directory and tracked exclusions; uploaded collection files on S3 require manual backup. Schedule/retain deliberately and protect archives. +- pb-backup-auth (REASONED; pb): Backup downloads require a superuser file token and obey superuser IP restrictions; ordinary auth/file tokens do not suffice. +- pb-limiter (REASONED; pb, pb-docs): Native limits exist from v0.23.0 but default disabled at v0.40.4; enable rateLimits.enabled. +- pb-limit-rules (REASONED; pb): Seeded limits are *:auth 2/3s, *:create 20/5s, /api/batch 3/1s and /api/ 300/10s. +- pb-limit-bypass (REASONED; pb): Superusers and rateLimits.excludedIPs bypass limits; test ordinary non-excluded clients. +- pb-smtp (REASONED; pb): smtp.enabled and smtp.tls default false; disabled SMTP falls back to sendmail. Configure protected transport and test actual delivery. +- appwrite-https (REASONED; appwrite, appwrite-docs): API FORCE_HTTPS and function/site ROUTER_FORCE_HTTPS default disabled at 2.2.0; explicitly enable both despite conflicting rolling deprecation/default documentation. +- appwrite-hosts (REASONED; appwrite): ROUTER_PROTECTION defaults disabled; configure actual domains and enable rejection of unknown hostnames. +- appwrite-registration (REASONED; appwrite): Console root-only registration defaults enabled with empty email/IP allowlists; these restrict account creation, not dashboard reachability or login. +- appwrite-console (REASONED; appwrite, appwrite-docs): Claim the first operator privately and use a separate network/access boundary for a private dashboard. +- appwrite-mfa (REASONED; appwrite, appwrite-docs): Enroll and verify operator TOTP, protect recovery codes; at 2.2.0 enabled MFA with a verified factor requires a second session factor. +- appwrite-dev-env (REASONED; appwrite): Development env disables root-only registration, abuse and router protection and carries placeholders; do not inherit it for production. +- appwrite-recreate (REASONED; appwrite-docs): Apply env/Compose changes with docker compose up -d from the install directory, then check effective behavior. +- appwrite-permissions (REASONED; appwrite, appwrite-docs): Collection and document grants are additive according to legacy documentation; documentSecurity defaults false in Appwrite 2.2.0 and must be enabled for document grants. The pinned parameter description states the need for one permission level. +- appwrite-creation (REASONED; appwrite, appwrite-docs, utopia-php-database): For null or omitted document permissions, the pinned aggregate helper preserves null before the handler assigns read/update/delete user grants to a nonprivileged requester with a user ID, or an empty list otherwise. API-key classification skips role validation but does not itself determine the default list. Use specific users/teams for private data. +- appwrite-read-path (REASONED; appwrite): Document GET classifies the requester from authorization roles (keys; owner/developer/admin privileged), reports disabled databases/collections as not found except to those requesters, and fetches the document through the database layer; collection permissions and documentSecurity are passed to the database layer at collection creation. +- appwrite-requester (REASONED; appwrite): With an API key the request role and scopes come from the key; a standard or ephemeral project key for the same project disables the default authorization status, as do admin-mode console sessions outside console project routes, while route scope validation applies to every requester. +- appwrite-keys (REASONED; appwrite, appwrite-docs): Properly scoped server API keys bypass resource permissions but still obey operation scopes; keys.write is admin-equivalent and keys never belong in clients. +- appwrite-encryption (REASONED; appwrite, appwrite-docs): Replace _APP_OPENSSL_KEY_V1=your-secret-key before production data; preserve the exact key separately, since replacement/loss strands encrypted secrets. +- appwrite-backups (REASONED; appwrite-docs): Back up database, persistent storage and deployment configuration; restrict access and test restoration in a separate installation. +- appwrite-abuse (REASONED; appwrite, appwrite-docs): _APP_OPTIONS_ABUSE defaults enabled but dev env disables it; server API-key requests are exempt, so test with ordinary clients. +- appwrite-smtp (REASONED; appwrite, appwrite-docs): SMTP host/port/secure/username/password default empty and empty host disables sending; configure the provider and confirm message receipt/use. +- appwrite-storage (REASONED; appwrite): Storage defaults local, upload limit 30000000 and server antivirus disabled; enabling scanning requires configured reachable ClamAV. +- appwrite-bucket (REASONED; appwrite): New bucket permissions are empty and fileSecurity=false; enable per-file security deliberately, since bucket grants also permit access. +- appwrite-bucket-options (REASONED; appwrite): Bucket encryption and antivirus default true; bucket antivirus does not enable the server scanner. +- appwrite-size (REASONED; appwrite): Encryption/scanning skip files above 20000000 bytes; cap maximumFileSize at that threshold when either is mandatory and test the boundary. +- appwrite-functions (REASONED; appwrite): Function execute roles and execution-key scopes default empty; grant narrowly. Disabled functions still admit authorized Server SDK API keys. +- appwrite-executor (REASONED; appwrite, appwrite-docs): Keep executor/orchestrator private, protect Docker-socket authority and replace _APP_EXECUTOR_SECRET; container execution is not demonstrated hostile-tenant isolation. +- verify-pb-records (REASONED; pb-docs, curl): Public rules allow guest reads; locked rules deny with 403, unsatisfied list filters can return empty 200 and view rules 404. Retain owner and separate superuser controls. +- verify-appwrite-records (REASONED; appwrite-docs): Broad collection grants defeat document restrictions; removing them must deny unrelated users while document grantees succeed. Compare client/server creation defaults. +- verify-appwrite-keys (REASONED; appwrite): A correctly scoped server key reads despite empty permissions, while a key without scope fails; neither substitutes for an ordinary-user control. +- verify-registration (REASONED; appwrite): On disposable Appwrite fixtures compare open signup 201 with root-only account-limit failure and independent email/IP restrictions; retain login/invitation controls. +- verify-pb-bootstrap (REASONED; pb): Guest and ordinary-user _superusers record creation must fail while a valid installer token creates the private operator; static dashboard assets prove no admin access. +- verify-appwrite-mfa (REASONED; appwrite, appwrite-docs): Console account GET succeeds without MFA, fails with user_more_factors_required for a fresh password-only MFA session, and succeeds after TOTP completion. +- verify-pb-writes (REASONED; pb): Compare intended/unauthorized POST/PATCH/DELETE and privileged auth-record management; locked manageRule grants no management and empty-string configuration must fail. +- verify-pb-files (REASONED; pb, curl): Before protection unsigned file GET works; Protected plus private viewRule rejects guests/unrelated-user file tokens with 404 while permitted-user bytes still arrive. +- verify-pb-realtime (REASONED; pb, pb-docs): Mutate a fixture while comparing collection and record subscriptions; deny unauthorized event delivery, retaining authorized delivery. PB_CONNECT/acknowledgment alone is insufficient. +- verify-pb-mfa (REASONED; pb-docs): Compare password-only completion with password-plus-OTP MFA and verified/unverified authRule logins; clearing authStore must not revoke a retained valid token. +- verify-pb-ips (REASONED; pb): Compare allowed/excluded superuser sources and forged forwarded headers; excluded sources must get 403 and direct backend access must fail with a working proxy control. +- verify-transport (REASONED; pb, appwrite): Compare HTTP/HTTPS and certificates, Appwrite API/function/site domains and configured/unknown Host; compare browser origins separately from record authorization. +- verify-abuse (REASONED; pb, appwrite-docs): Cross each configured threshold with ordinary non-excluded clients and compare disabled/enabled controls; PocketBase limiting yields 429 with later successful recovery. +- verify-email (REASONED; pb, appwrite-docs): Request OTP, verification and recovery messages and confirm receipt and use; request success alone is not delivery evidence. +- verify-pb-recovery (REASONED; pb, pb-docs): Compare plaintext/encrypted persisted settings, restart with retained key and restore local/S3 data; only permitted superuser file tokens may download backups. +- verify-appwrite-recovery (REASONED; appwrite-docs): Restore encrypted fixtures with the original key; a separate wrong-key fixture must not recover encrypted values. +- verify-uploads (REASONED; appwrite): Compare permitted/unrelated uploads and size-boundary fixtures; mandatory processing requires rejecting over-limit files and confirming accepted-file encryption/scanning. +- verify-functions (REASONED; appwrite): Compare intended execute roles, unrelated users and privileged server keys, including disabled functions; confirm executor/orchestrator privacy from a second host. +- verify-bundle (REASONED; pb-docs, appwrite-docs): Search filenames using protected exact-secret patterns with a disposable marker control; client bundles must contain neither superuser tokens nor Appwrite API keys, and encoded/split forms can evade scanning. +- local-guards (DEMONSTRATED; pb-docs, appwrite-docs): Recorded guard tests rejected placeholders and omitted/shortened arguments; this demonstrates shell behavior only, not service access or deployed-bundle scanning. +- local-syntax (DEMONSTRATED; appwrite, pb-docs, appwrite-docs): All three bash fences have recorded local lint/syntax success, not live service evidence; ShellCheck version is recorded only in the body. +- source-limits (REASONED; appwrite-docs, pb-docs, appwrite): The PocketBase CLI wrapper, --dev default, and Appwrite requester classification and document-read call sites are now cited; how utopia-php/database matches roles against collection/document permissions (including documentSecurity) and what the disabled default authorization status bypasses remain untraced in that unfetched dependency. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "56cf568348e9a9e7890afd9133b526447b70bf593b07da5b93bedb63ce116e85", + "components": { + "pb": { + "name": "PocketBase", + "basis": "v0.40.4", + "sources": { + "saf684a26056b": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/installer.go", + "s4b155ef30262": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/cmd/serve.go", + "sb2129128b86c": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/serve.go", + "s5b66ca69bf6d": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/middlewares.go", + "s04ae861b5709": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/record_query.go", + "s69aa223f52e4": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/record_crud.go", + "sce114368c2ad": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/collection_model_auth_options.go", + "s03dd7dd84c2a": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/settings_model.go", + "seb56d3763223": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/base.go", + "sa5e571f8d473": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/file.go", + "sfd2f779e8e11": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/realtime.go", + "s31a5adccd695": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/backup.go", + "se74188862a55": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/backup_create.go#L36-L60", + "sf6ffdca4dd39": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/middlewares_rate_limit.go", + "s1b04c2cd16ec": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L234-L239", + "sb63f49346abe": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L73-L79", + "sd700e81880f2": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L129-L139", + "s54430c83a38a": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L165-L173", + "s2c8f78f2d5ac": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/tools/osutils/run.go#L10-L19", + "s5b0dd2fd7a6a": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/base.go#L1277-L1286", + "sf7c2fb5e78e7": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/base.go#L1489-L1499" + } + }, + "appwrite": { + "name": "Appwrite", + "basis": "2.2.0", + "sources": { + "s9f5bd3b70f14": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/config/variables.php", + "sadb8c6e7b00a": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/.env", + "s2bb21218c5ee": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/api/account.php", + "s193bfa5c6e22": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/shared/api.php", + "s427bab3733b6": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/general.php", + "se9480e5807b3": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Create.php", + "sf916851a0b10": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php#L82-L84", + "s5b4b3eb2271f": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L129-L134", + "s93647e16b966": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L211-L212", + "sda698c40fc34": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L237-L287", + "s655a0abb9ef0": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php", + "s4ab95ecbf6b5": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/init/constants.php", + "s8e5272615f2e": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/docker-compose.yml", + "s7ba6f9c71972": "https://github.com/appwrite/appwrite/blob/2.2.0/composer.lock#L3461-L3471", + "s5396e119c026": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php#L80-L95", + "s82988de645fa": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php#L103-L120", + "s224cc9d8a387": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Utopia/Database/Documents/User.php#L75-L109", + "sc55465cf878d": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L144-L161", + "sed1bf9453ecb": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L312-L339", + "s8784d02c82c9": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L357-L361", + "sf60a2b283b80": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L503-L507", + "s1fe0e6185518": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php#L221-L227" + } + }, + "pb-docs": { + "name": "PocketBase documentation (rolling)", + "basis": "unknown", + "sources": { + "s9029ab974cf1": "https://pocketbase.io/docs/going-to-production/", + "saedbdfbdfc48": "https://pocketbase.io/docs/api-rules-and-filters/", + "s773e441dd51c": "https://pocketbase.io/docs/authentication/", + "sf711b6345643": "https://pocketbase.io/docs/files-handling/", + "sb850a71fad4d": "https://pocketbase.io/docs/api-realtime/" + } + }, + "appwrite-docs": { + "name": "Appwrite documentation (rolling)", + "basis": "unknown", + "sources": { + "s4a965bdeeb98": "https://appwrite.io/docs/advanced/self-hosting/production/security", + "saa37bb5d4d7e": "https://appwrite.io/docs/partners/project/api-keys", + "sa0bd132f7968": "https://appwrite.io/docs/advanced/security/permissions", + "s58ad28816c5d": "https://appwrite.io/docs/products/databases/tablesdb/legacy/permissions", + "s2bf9a4746d28": "https://appwrite.io/docs/advanced/self-hosting/configuration/environment-variables", + "s000d05106450": "https://appwrite.io/docs/advanced/security/rate-limits", + "s38a1efa0c7d2": "https://appwrite.io/docs/advanced/self-hosting/configuration/email", + "s256ef73f7b85": "https://appwrite.io/docs/advanced/self-hosting/production/backups", + "s221c6aaef8df": "https://appwrite.io/docs/advanced/security/mfa" + } + }, + "utopia-php-database": { + "name": "utopia-php/database", + "basis": "7.3.9", + "sources": { + "s847e0ded0300": "https://github.com/utopia-php/database/blob/54364c687d6ddb3d976d7130b9001c0b29276933/src/Database/Helpers/Permission.php#L151-L155" + } + }, + "curl": { + "name": "curl", + "basis": "curl-8_14_1", + "sources": { + "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", + "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md", + "s82a34ee18632": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/config.md" + } + } + }, + "claims": { + "pb-bootstrap": {"text": "Privately bootstrap through the logged installer URL; superuser create EMAIL PASS exposes passwords in argv. Ordinary registration does not grant superuser rights.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:saf684a26056b"], "status": "REASONED"}, + "pb-installer": {"text": "Installer system-superuser token lasts about 30 minutes; protect its URL and startup logs as credentials.", "components": ["pb"], "sources": ["pb:saf684a26056b"], "status": "REASONED"}, + "pb-ips": {"text": "Superuser IP allowlists are available from v0.38.0; superuserIPs defaults empty and restricts authenticated requests, not /_/ static assets.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s5b66ca69bf6d"], "status": "REASONED"}, + "pb-tls": {"text": "serve with a domain provides native HTTPS/ACME; alternatively terminate TLS at a reverse proxy.", "components": ["pb"], "sources": ["pb:sb2129128b86c"], "status": "REASONED"}, + "pb-bind": {"text": "Without a domain serve defaults to 127.0.0.1:8090; with a domain it uses 0.0.0.0:80/443. Behind a local proxy omit the domain and set --http loopback.", "components": ["pb"], "sources": ["pb:s4b155ef30262"], "status": "REASONED"}, + "pb-origins": {"text": "--origins defaults * and accepts comma-separated allowed origins; CORS does not authorize records.", "components": ["pb"], "sources": ["pb:s4b155ef30262"], "status": "REASONED"}, + "pb-proxy": {"text": "Trust only headers the proxy overwrites, review useLeftmostIP ordering and block direct backend access to protect IP/rate controls.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a"], "status": "REASONED"}, + "pb-dev": {"text": "--dev adds diagnostics including SQL on stderr, not an auth bypass; its configured default comes from New(): true if os.Args[0] starts with a nonempty os.TempDir() or cacheDir() prefix, false otherwise.", "components": ["pb"], "sources": ["pb:seb56d3763223", "pb:s1b04c2cd16ec", "pb:sb63f49346abe", "pb:s2c8f78f2d5ac", "pb:sd700e81880f2", "pb:s5b0dd2fd7a6a", "pb:sf7c2fb5e78e7"], "status": "REASONED"}, + "pb-cli": {"text": "In the pinned CLI wrapper, Start() registers the serve and superuser commands on the root command and executes it.", "components": ["pb"], "sources": ["pb:s54430c83a38a"], "status": "REASONED"}, + "pb-rules": {"text": "Each list/view/create/update/delete rule defaults null (superuser-only); empty opens to guests and nonempty filters. Superusers bypass rules.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:saedbdfbdfc48", "pb:s04ae861b5709"], "status": "REASONED"}, + "pb-manage": {"text": "Top-level manageRule defaults null and permits privileged auth-record changes alongside create/update rules; its validator rejects empty-string rules.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-files": {"text": "File fields default unprotected despite locked record rules; Protected plus an authorized viewRule gates downloads, while a public viewRule still permits public access.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sf711b6345643", "pb:sa5e571f8d473"], "status": "REASONED"}, + "pb-file-token": {"text": "Short-lived file tokens provide identity context, not file authorization; viewRule decides. Protect token-bearing file URLs.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sf711b6345643", "pb:sa5e571f8d473"], "status": "REASONED"}, + "pb-realtime": {"text": "Collection subscriptions use listRule and individual-record subscriptions viewRule; SSE/subscription success does not prove event authorization.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sb850a71fad4d", "pb:sfd2f779e8e11"], "status": "REASONED"}, + "pb-password": {"text": "Auth initialization enables password authentication with email as the identity field.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-oauth": {"text": "OAuth2 defaults disabled and is unsupported for _superusers.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, + "pb-mfa-default": {"text": "MFA defaults disabled with duration 600 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-otp-default": {"text": "OTP defaults disabled with duration 180 seconds and length 8.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-alerts": {"text": "Authentication alerts default enabled; retain alerts and provide working email.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-auth-token": {"text": "Auth token duration defaults to 432000 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-file-duration": {"text": "File token duration defaults to 180 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-auth-rule": {"text": "authRule defaults empty; verified-only collections can require verified = true after email verification works.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, + "pb-mfa": {"text": "For superusers retain password auth and enable both OTP and MFA; empty mfa.rule applies to everyone. OTP alone is not a second factor.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, + "pb-logout": {"text": "Stateless authentication means clearing pb.authStore removes only the local copy, not a stolen token.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s773e441dd51c", "pb:s04ae861b5709"], "status": "REASONED"}, + "pb-encryption": {"text": "Settings default to plaintext JSON; select a 32-character environment secret with --encryptionEnv. This encrypts settings, not the whole database or files.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a"], "status": "REASONED"}, + "pb-backups": {"text": "Backups default local with empty cron (automatic backups off); archives contain main and logs databases plus remaining pb_data, excluding the backups directory and tracked exclusions; uploaded collection files on S3 require manual backup. Schedule/retain deliberately and protect archives.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a", "pb:se74188862a55"], "status": "REASONED"}, + "pb-backup-auth": {"text": "Backup downloads require a superuser file token and obey superuser IP restrictions; ordinary auth/file tokens do not suffice.", "components": ["pb"], "sources": ["pb:s31a5adccd695"], "status": "REASONED"}, + "pb-limiter": {"text": "Native limits exist from v0.23.0 but default disabled at v0.40.4; enable rateLimits.enabled.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a"], "status": "REASONED"}, + "pb-limit-rules": {"text": "Seeded limits are *:auth 2/3s, *:create 20/5s, /api/batch 3/1s and /api/ 300/10s.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a"], "status": "REASONED"}, + "pb-limit-bypass": {"text": "Superusers and rateLimits.excludedIPs bypass limits; test ordinary non-excluded clients.", "components": ["pb"], "sources": ["pb:sf6ffdca4dd39"], "status": "REASONED"}, + "pb-smtp": {"text": "smtp.enabled and smtp.tls default false; disabled SMTP falls back to sendmail. Configure protected transport and test actual delivery.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a", "pb:seb56d3763223"], "status": "REASONED"}, + "appwrite-https": {"text": "API FORCE_HTTPS and function/site ROUTER_FORCE_HTTPS default disabled at 2.2.0; explicitly enable both despite conflicting rolling deprecation/default documentation.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s427bab3733b6", "appwrite-docs:s2bf9a4746d28"], "status": "REASONED"}, + "appwrite-hosts": {"text": "ROUTER_PROTECTION defaults disabled; configure actual domains and enable rejection of unknown hostnames.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s427bab3733b6"], "status": "REASONED"}, + "appwrite-registration": {"text": "Console root-only registration defaults enabled with empty email/IP allowlists; these restrict account creation, not dashboard reachability or login.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s2bb21218c5ee"], "status": "REASONED"}, + "appwrite-console": {"text": "Claim the first operator privately and use a separate network/access boundary for a private dashboard.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s4a965bdeeb98", "appwrite:s2bb21218c5ee"], "status": "REASONED"}, + "appwrite-mfa": {"text": "Enroll and verify operator TOTP, protect recovery codes; at 2.2.0 enabled MFA with a verified factor requires a second session factor.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s221c6aaef8df", "appwrite:s193bfa5c6e22"], "status": "REASONED"}, + "appwrite-dev-env": {"text": "Development env disables root-only registration, abuse and router protection and carries placeholders; do not inherit it for production.", "components": ["appwrite"], "sources": ["appwrite:sadb8c6e7b00a"], "status": "REASONED"}, + "appwrite-recreate": {"text": "Apply env/Compose changes with docker compose up -d from the install directory, then check effective behavior.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s4a965bdeeb98"], "status": "REASONED"}, + "appwrite-permissions": {"text": "Collection and document grants are additive according to legacy documentation; documentSecurity defaults false in Appwrite 2.2.0 and must be enabled for document grants. The pinned parameter description states the need for one permission level.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:sf916851a0b10", "appwrite-docs:s58ad28816c5d"], "status": "REASONED"}, + "appwrite-creation": {"text": "For null or omitted document permissions, the pinned aggregate helper preserves null before the handler assigns read/update/delete user grants to a nonprivileged requester with a user ID, or an empty list otherwise. API-key classification skips role validation but does not itself determine the default list. Use specific users/teams for private data.", "components": ["appwrite", "appwrite-docs", "utopia-php-database"], "sources": ["appwrite:s5b4b3eb2271f", "appwrite:s93647e16b966", "appwrite:sda698c40fc34", "appwrite-docs:sa0bd132f7968", "appwrite:s7ba6f9c71972", "utopia-php-database:s847e0ded0300"], "status": "REASONED"}, + "appwrite-read-path": {"text": "Document GET classifies the requester from authorization roles (keys; owner/developer/admin privileged), reports disabled databases/collections as not found except to those requesters, and fetches the document through the database layer; collection permissions and documentSecurity are passed to the database layer at collection creation.", "components": ["appwrite"], "sources": ["appwrite:s5396e119c026", "appwrite:s82988de645fa", "appwrite:s224cc9d8a387", "appwrite:s1fe0e6185518"], "status": "REASONED"}, + "appwrite-requester": {"text": "With an API key the request role and scopes come from the key; a standard or ephemeral project key for the same project disables the default authorization status, as do admin-mode console sessions outside console project routes, while route scope validation applies to every requester.", "components": ["appwrite"], "sources": ["appwrite:sc55465cf878d", "appwrite:sed1bf9453ecb", "appwrite:s8784d02c82c9", "appwrite:sf60a2b283b80"], "status": "REASONED"}, + "appwrite-keys": {"text": "Properly scoped server API keys bypass resource permissions but still obey operation scopes; keys.write is admin-equivalent and keys never belong in clients.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s193bfa5c6e22", "appwrite-docs:saa37bb5d4d7e"], "status": "REASONED"}, + "appwrite-encryption": {"text": "Replace _APP_OPENSSL_KEY_V1=your-secret-key before production data; preserve the exact key separately, since replacement/loss strands encrypted secrets.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, + "appwrite-backups": {"text": "Back up database, persistent storage and deployment configuration; restrict access and test restoration in a separate installation.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, + "appwrite-abuse": {"text": "_APP_OPTIONS_ABUSE defaults enabled but dev env disables it; server API-key requests are exempt, so test with ordinary clients.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:sadb8c6e7b00a", "appwrite:s193bfa5c6e22", "appwrite-docs:s000d05106450"], "status": "REASONED"}, + "appwrite-smtp": {"text": "SMTP host/port/secure/username/password default empty and empty host disables sending; configure the provider and confirm message receipt/use.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite-docs:s38a1efa0c7d2"], "status": "REASONED"}, + "appwrite-storage": {"text": "Storage defaults local, upload limit 30000000 and server antivirus disabled; enabling scanning requires configured reachable ClamAV.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14"], "status": "REASONED"}, + "appwrite-bucket": {"text": "New bucket permissions are empty and fileSecurity=false; enable per-file security deliberately, since bucket grants also permit access.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3"], "status": "REASONED"}, + "appwrite-bucket-options": {"text": "Bucket encryption and antivirus default true; bucket antivirus does not enable the server scanner.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3"], "status": "REASONED"}, + "appwrite-size": {"text": "Encryption/scanning skip files above 20000000 bytes; cap maximumFileSize at that threshold when either is mandatory and test the boundary.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3", "appwrite:s4ab95ecbf6b5"], "status": "REASONED"}, + "appwrite-functions": {"text": "Function execute roles and execution-key scopes default empty; grant narrowly. Disabled functions still admit authorized Server SDK API keys.", "components": ["appwrite"], "sources": ["appwrite:s655a0abb9ef0"], "status": "REASONED"}, + "appwrite-executor": {"text": "Keep executor/orchestrator private, protect Docker-socket authority and replace _APP_EXECUTOR_SECRET; container execution is not demonstrated hostile-tenant isolation.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s8e5272615f2e", "appwrite-docs:s2bf9a4746d28"], "status": "REASONED"}, + "verify-pb-records": {"text": "Public rules allow guest reads; locked rules deny with 403, unsatisfied list filters can return empty 200 and view rules 404. Retain owner and separate superuser controls.", "components": ["pb-docs", "curl"], "sources": ["pb-docs:saedbdfbdfc48", "curl:s5e1383feef7d", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, + "verify-appwrite-records": {"text": "Broad collection grants defeat document restrictions; removing them must deny unrelated users while document grantees succeed. Compare client/server creation defaults.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s58ad28816c5d", "appwrite-docs:sa0bd132f7968"], "status": "REASONED", "verify": [1]}, + "verify-appwrite-keys": {"text": "A correctly scoped server key reads despite empty permissions, while a key without scope fails; neither substitutes for an ordinary-user control.", "components": ["appwrite"], "sources": ["appwrite:s193bfa5c6e22"], "status": "REASONED", "verify": [1]}, + "verify-registration": {"text": "On disposable Appwrite fixtures compare open signup 201 with root-only account-limit failure and independent email/IP restrictions; retain login/invitation controls.", "components": ["appwrite"], "sources": ["appwrite:s2bb21218c5ee"], "status": "REASONED", "verify": [2]}, + "verify-pb-bootstrap": {"text": "Guest and ordinary-user _superusers record creation must fail while a valid installer token creates the private operator; static dashboard assets prove no admin access.", "components": ["pb"], "sources": ["pb:saf684a26056b", "pb:s69aa223f52e4"], "status": "REASONED"}, + "verify-appwrite-mfa": {"text": "Console account GET succeeds without MFA, fails with user_more_factors_required for a fresh password-only MFA session, and succeeds after TOTP completion.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s221c6aaef8df", "appwrite:s193bfa5c6e22"], "status": "REASONED"}, + "verify-pb-writes": {"text": "Compare intended/unauthorized POST/PATCH/DELETE and privileged auth-record management; locked manageRule grants no management and empty-string configuration must fail.", "components": ["pb"], "sources": ["pb:s69aa223f52e4", "pb:sce114368c2ad"], "status": "REASONED"}, + "verify-pb-files": {"text": "Before protection unsigned file GET works; Protected plus private viewRule rejects guests/unrelated-user file tokens with 404 while permitted-user bytes still arrive.", "components": ["pb", "curl"], "sources": ["pb:sa5e571f8d473", "curl:s82a34ee18632"], "status": "REASONED"}, + "verify-pb-realtime": {"text": "Mutate a fixture while comparing collection and record subscriptions; deny unauthorized event delivery, retaining authorized delivery. PB_CONNECT/acknowledgment alone is insufficient.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sb850a71fad4d", "pb:sfd2f779e8e11"], "status": "REASONED"}, + "verify-pb-mfa": {"text": "Compare password-only completion with password-plus-OTP MFA and verified/unverified authRule logins; clearing authStore must not revoke a retained valid token.", "components": ["pb-docs"], "sources": ["pb-docs:s773e441dd51c"], "status": "REASONED"}, + "verify-pb-ips": {"text": "Compare allowed/excluded superuser sources and forged forwarded headers; excluded sources must get 403 and direct backend access must fail with a working proxy control.", "components": ["pb"], "sources": ["pb:s5b66ca69bf6d", "pb:s03dd7dd84c2a"], "status": "REASONED"}, + "verify-transport": {"text": "Compare HTTP/HTTPS and certificates, Appwrite API/function/site domains and configured/unknown Host; compare browser origins separately from record authorization.", "components": ["pb", "appwrite"], "sources": ["pb:s4b155ef30262", "appwrite:s427bab3733b6"], "status": "REASONED"}, + "verify-abuse": {"text": "Cross each configured threshold with ordinary non-excluded clients and compare disabled/enabled controls; PocketBase limiting yields 429 with later successful recovery.", "components": ["pb", "appwrite-docs"], "sources": ["pb:sf6ffdca4dd39", "appwrite-docs:s000d05106450"], "status": "REASONED"}, + "verify-email": {"text": "Request OTP, verification and recovery messages and confirm receipt and use; request success alone is not delivery evidence.", "components": ["pb", "appwrite-docs"], "sources": ["pb:seb56d3763223", "appwrite-docs:s38a1efa0c7d2"], "status": "REASONED"}, + "verify-pb-recovery": {"text": "Compare plaintext/encrypted persisted settings, restart with retained key and restore local/S3 data; only permitted superuser file tokens may download backups.", "components": ["pb", "pb-docs"], "sources": ["pb:s03dd7dd84c2a", "pb:s31a5adccd695", "pb-docs:s9029ab974cf1"], "status": "REASONED"}, + "verify-appwrite-recovery": {"text": "Restore encrypted fixtures with the original key; a separate wrong-key fixture must not recover encrypted values.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, + "verify-uploads": {"text": "Compare permitted/unrelated uploads and size-boundary fixtures; mandatory processing requires rejecting over-limit files and confirming accepted-file encryption/scanning.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3", "appwrite:s4ab95ecbf6b5"], "status": "REASONED"}, + "verify-functions": {"text": "Compare intended execute roles, unrelated users and privileged server keys, including disabled functions; confirm executor/orchestrator privacy from a second host.", "components": ["appwrite"], "sources": ["appwrite:s655a0abb9ef0", "appwrite:s8e5272615f2e"], "status": "REASONED"}, + "verify-bundle": {"text": "Search filenames using protected exact-secret patterns with a disposable marker control; client bundles must contain neither superuser tokens nor Appwrite API keys, and encoded/split forms can evade scanning.", "components": ["pb-docs", "appwrite-docs"], "sources": ["pb-docs:s773e441dd51c", "appwrite-docs:saa37bb5d4d7e"], "status": "REASONED", "verify": [3]}, + "local-guards": {"text": "Recorded guard tests rejected placeholders and omitted/shortened arguments; this demonstrates shell behavior only, not service access or deployed-bundle scanning.", "components": ["pb-docs", "appwrite-docs"], "sources": ["pb-docs:saedbdfbdfc48", "appwrite-docs:sa0bd132f7968", "appwrite-docs:saa37bb5d4d7e"], "status": "DEMONSTRATED", "evidence": "Local guard tests refused embedded `REPLACE_WITH_` placeholders, `example.com`, angle brackets, empty arguments, and omitted or shortened `set --` lines."}, + "local-syntax": {"text": "All three bash fences have recorded local lint/syntax success, not live service evidence; ShellCheck version is recorded only in the body.", "components": ["appwrite", "pb-docs", "appwrite-docs"], "sources": ["pb-docs:saedbdfbdfc48", "appwrite:s2bb21218c5ee", "appwrite-docs:saa37bb5d4d7e"], "status": "DEMONSTRATED", "evidence": "All three bash blocks passed ShellCheck 0.11.0 and `bash -n` during authoring."}, + "source-limits": {"text": "The PocketBase CLI wrapper, --dev default, and Appwrite requester classification and document-read call sites are now cited; how utopia-php/database matches roles against collection/document permissions (including documentSecurity) and what the disabled default authorization status bypasses remain untraced in that unfetched dependency.", "components": ["appwrite-docs", "pb-docs", "appwrite"], "sources": ["appwrite-docs:s58ad28816c5d", "pb-docs:s9029ab974cf1", "appwrite:s7ba6f9c71972"], "status": "REASONED"} + } +} +```` + diff --git a/ray.md b/ray.md index 2097d9418..acb300bdc 100644 --- a/ray.md +++ b/ray.md @@ -1,317 +1,18 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-26", - "documentation_checked": "2026-09", - "body_sha256": "286cb3508e87ad0658bd57c73270ab3b4100681250bae339f4d71a573e537bfb", - "components": { - "ray": { - "name": "Ray pinned source", - "basis": "ray-2.58.0", - "sources": { - "sed3f197cc1ef": "https://github.com/ray-project/ray/blob/ray-2.58.0/doc/source/ray-security/token-auth.md", - "s08d31327265a": "https://raw.githubusercontent.com/ray-project/ray/ray-2.58.0/docker/base-deps/Dockerfile", - "s2740a1880e2b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/rpc/grpc_server.cc#L67-L68", - "s958054ab801b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.h#L111-L113", - "sa3d2966f536b": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L601-L617", - "s050b349f5bbd": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L185", - "sd7d2d0d963d0": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.cc#L257-L278", - "s329ab4e0ed63": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/serve/_private/replica.py#L1778", - "s3eb2c2cb2cbb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/util/client/server/server.py#L799-L801", - "sae1bb5af6e35": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/util/client/server/proxier.py#L929-L931", - "s207ba00c4e86": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1572-L1575", - "s3efda42d7913": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L2464-L2471", - "s9a060fe08a53": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L189", - "sb1935b32e677": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L618-L635", - "se9af8ec90932": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L687-L692", - "s17bc66b5543e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L20-L23", - "s7df4fce455ca": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L54-L66", - "sc96f32b8a459": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L107-L113", - "sbca5ef4de0bb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/authentication/http_token_authentication.py#L28-L80", - "scdfff3ba79f6": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/optional_utils.py#L132-L209", - "s528944bdec23": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_agent.py#L32-L196", - "s4796e2ca38da": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_head.py#L123-L144", - "s1d86b2f31870": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_head.py#L267-L280", - "s9976ecf5b1f1": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L245-L249", - "sa542601368d1": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L264-L307", - "sd12bacfbc3c7": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L345-L414", - "s7ef1edbcda5c": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L570-L638", - "s6f5310eb313d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L144-L169", - "s8f508c7e2c94": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L182-L187", - "s48b3469099a3": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L100-L117", - "s532a98227d6d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L372-L377", - "s33a5ff7c987b": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1660-L1669", - "s1a19fd793b8f": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1768", - "s8f1ebe4bd911": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1381-L1387", - "s7682a13e83a2": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1862-L1883", - "s9237e74ea054": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1918-L1929", - "s4ce6fe077ba9": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L2022-L2031", - "s7f27125f73b7": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L504-L516", - "sf921a5d6a898": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/prometheus_exporter.py#L326-L334", - "s434c7b4364dc": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L218-L224", - "s08aa128e5d70": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L243-L248", - "sdd8960062a9c": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/includes/network_util.pxi#L103-L110", - "s51476b743c19": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.cc#L280-L289", - "s7451c8e00be3": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L850-L854", - "s6c7faadf88a4": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L786-L821", - "s3625ec280c5e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/worker.py#L1835-L1836", - "sd3a18ab2a112": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L507-L514", - "sba3f67581471": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/parameter.py#L165-L167", - "s994d0ddb6a7d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/worker.py#L1884-L1912", - "sd4d44da36278": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/utils.py#L1128-L1142", - "s5a9d9c171f49": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1909-L1913", - "s9cd95d66d32a": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/utils.py#L322-L351", - "s762672f680e9": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L2099-L2101", - "s28e3a1076d01": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L23-L33", - "s883e3e00e4df": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/common/ray_config_def.h#L618-L619", - "sdb1c4564137b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/raylet/node_manager.cc#L3527-L3530", - "sa7d4f677aadb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_agent.py#L198-L203", - "sa5873642c8a2": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_manager.py#L421-L463", - "s2d562f0e7567": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_manager.py#L565-L609", - "s3a079f7e3d82": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/prometheus_exporter.py#L10", - "sefae1a66c0dd": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L102-L114", - "scc7af6be456e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/serve/config.py#L955-L963" - } - }, - "docs": { - "name": "Ray documentation", - "basis": "unknown", - "sources": { - "sa5e2c2d58c12": "https://docs.ray.io/en/latest/ray-security/index.html", - "s6f0aa11f549a": "https://docs.ray.io/en/latest/ray-security/token-auth.html", - "s56c7c98ab722": "https://docs.ray.io/en/latest/cluster/cli.html", - "s261570b2672b": "https://docs.ray.io/en/latest/ray-core/configure.html", - "sc375ff2ca01f": "https://docs.ray.io/en/latest/ray-core/handling-dependencies.html", - "sb7a1d0b7dfb3": "https://docs.ray.io/en/latest/ray-core/api/doc/ray.runtime_env.RuntimeEnv.html", - "scd3f7d8a9b02": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.config.HTTPOptions.html", - "s15aa6ec80fb8": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.schema.HTTPOptionsSchema.html", - "s045d75fbdecc": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.schema.ServeDeploySchema.html", - "sc98a37c2b157": "https://docs.ray.io/en/latest/serve/http-guide.html", - "s54ed4b890b65": "https://docs.ray.io/en/latest/cluster/kubernetes/user-guides/kuberay-gcs-ft.html" - } - }, - "token-min": { - "name": "Ray token authentication introduction", - "basis": "2.52.0", - "sources": { - "sa5e2c2d58c12": "https://docs.ray.io/en/latest/ray-security/index.html" - } - }, - "redis-history": { - "name": "Ray Redis history announcement", - "basis": "unknown", - "sources": { - "sb5afda59324d": "https://www.anyscale.com/blog/redis-in-ray-past-and-future" - } - }, - "docker": { - "name": "Docker publishing documentation", - "basis": "unknown", - "sources": { - "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" - } - }, - "kuberay-auth": { - "name": "KubeRay authentication documentation", - "basis": "unknown", - "sources": { - "saf5247262bc7": "https://docs.ray.io/en/latest/cluster/kubernetes/user-guides/kuberay-auth.html" - } - }, - "kuberay": { - "name": "KubeRay chart", - "basis": "v1.7.0", - "sources": { - "s76e45858aa01": "https://github.com/ray-project/kuberay/blob/v1.7.0/helm-chart/ray-cluster/values.yaml" - } - }, - "kubernetes": { - "name": "Kubernetes security contexts", - "basis": "unknown", - "sources": { - "sb78ea91d0308": "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/" - } - }, - "python": { - "name": "CPython WSGI server", - "basis": "v3.11.0", - "sources": { - "sdd5a8e2ab59c": "https://github.com/python/cpython/blob/v3.11.0/Lib/wsgiref/simple_server.py#L1-L160", - "se4933ce3da65": "https://github.com/python/cpython/blob/v3.11.0/Lib/http/server.py#L120-L145", - "s0020052d7267": "https://github.com/python/cpython/blob/v3.11.0/Lib/socketserver.py#L400-L480" - } - }, - "curl": { - "name": "curl minimum write-out version", - "basis": "7.75.0", - "sources": { - "s2b2686afaf41": "https://curl.se/docs/manpage.html" - } - }, - "docker-release": { - "name": "Docker Engine release notes", - "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", - "sources": { - "sd733f57ded23": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/release-notes/28.md#L768-L768" - } - }, - "kuberay-auth-source": { - "name": "KubeRay authentication source", - "basis": "v1.6.0", - "sources": { - "s9a9f9edcc07e": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/apis/ray/v1/raycluster_types.go#L17-L19", - "s51c84c0bc3b4": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/apis/ray/v1/raycluster_types.go#L81-L107", - "s61e750cdc2e0": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/raycluster_controller.go#L369-L433", - "s6171707b3edb": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L194-L220", - "s408a3a202322": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L234-L236", - "s8505add03e5a": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L253-L271", - "s1cb4f972131a": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L296-L336", - "sd1b672bc4edd": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L460-L462", - "se37635162dc9": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/utils/util.go#L760-L766" - } - }, - "kuberay-auth-prior": { - "name": "KubeRay prior RayCluster types", - "basis": "v1.5.0", - "sources": { - "s04c5ce0b599d": "https://github.com/ray-project/kuberay/blob/v1.5.0/ray-operator/apis/ray/v1/raycluster_types.go#L12-L47" - } - } - }, - "claims": { - "boundary": {"text": "Dashboard, Jobs and Client access permits arbitrary code execution; use an isolated cluster network and separate clusters for mutually untrusted jobs.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12"], "status": "REASONED"}, - "dashboard": {"text": "Dashboard defaults to resolved localhost, IPv4 before IPv6 then 127.0.0.1, on port 8265; state loopback explicitly.", "components": ["ray"], "sources": ["ray:sa3d2966f536b", "ray:s050b349f5bbd", "ray:sd7d2d0d963d0"], "status": "REASONED"}, - "docker": {"text": "Bridge publishing uses host loopback and container-interface bind; this guide assesses Docker 28.0.0 or later with normal NAT as the loopback-isolation minimum because the 28.0.0 release notes record a fix for neighbour hosts reaching loopback-mapped ports.", "components": ["docker", "docker-release"], "sources": ["docker:s1e53417c513d", "docker-release:sd733f57ded23"], "status": "REASONED"}, - "access": {"text": "Reach Dashboard/Jobs through loopback SSH forwarding, launcher forwarding, KubeRay forwarding or a private tailnet; public browser access needs an authenticating TLS proxy.", "components": ["docs", "kuberay-auth"], "sources": ["docs:sa5e2c2d58c12", "docs:s56c7c98ab722", "kuberay-auth:saf5247262bc7"], "status": "REASONED"}, - "head-ports": {"text": "Head GCS uses 6379 and Client 10001; worker ports default 10002-19999 plus randomized listeners.", "components": ["docs"], "sources": ["docs:s56c7c98ab722", "docs:s261570b2672b"], "status": "REASONED"}, - "core-bind": {"text": "Core gRPC source binds all interfaces unless node address is exactly 127.0.0.1, ::1 or localhost; private node IP does not imply private bind.", "components": ["ray"], "sources": ["ray:s2740a1880e2b", "ray:s958054ab801b"], "status": "REASONED"}, - "gcs-observation": {"text": "Four attempted Ray 2.58.0 loopback starts still showed GCS *:6379; the cause was not established and the watcher stopped the runs.", "components": ["ray"], "sources": ["ray:s2740a1880e2b", "ray:s958054ab801b"], "status": "DEMONSTRATED", "evidence": "in each of four runs using `ray start --head --node-ip-address=127.0.0.1 --port=6379 --dashboard-host=127.0.0.1`, `ss` still showed the GCS listener as `*:6379`."}, - "client-bind": {"text": "Ray Client server/proxier listen on the supplied node address plus loopback; restrict access and prefer Jobs over publishing Client.", "components": ["ray"], "sources": ["ray:s3eb2c2cb2cbb", "ray:sae1bb5af6e35", "ray:s207ba00c4e86", "ray:s3efda42d7913"], "status": "REASONED"}, - "serve-replica": {"text": "Serve inter-deployment gRPC binds [::] on an OS-chosen port; this is source reasoning, not a run.", "components": ["ray"], "sources": ["ray:s329ab4e0ed63"], "status": "REASONED"}, - "agents": {"text": "--include-dashboard=false does not stop dashboard/runtime-env agents; head dashboard itself is reduced to usage stats.", "components": ["ray"], "sources": ["ray:s33a5ff7c987b", "ray:s1a19fd793b8f", "ray:s8f1ebe4bd911", "ray:s7682a13e83a2", "ray:s9237e74ea054", "ray:s4ce6fe077ba9"], "status": "REASONED"}, - "minimal": {"text": "Missing optional imports select minimal mode: no agent HTTP/gRPC or metrics reporter; runtime-env HTTP remains. Base installs may have full dependencies.", "components": ["ray"], "sources": ["ray:s48b3469099a3", "ray:sd4d44da36278", "ray:s5a9d9c171f49", "ray:s9cd95d66d32a", "ray:s762672f680e9", "ray:s28e3a1076d01"], "status": "REASONED"}, - "agent-http": {"text": "Non-minimal agent HTTP defaults 52365, including ray.init; --dashboard-agent-listen-port changes it, with node-IP plus conditional localhost binds.", "components": ["ray"], "sources": ["ray:s9a060fe08a53", "ray:sb1935b32e677", "ray:s7df4fce455ca", "ray:sba3f67581471", "ray:s994d0ddb6a7d"], "status": "REASONED"}, - "agent-jobs": {"text": "Direct /api/job_agent/jobs/ submission and stop/delete/log routes bypass a proxy on 8265; head forwarding reaches its agent job manager.", "components": ["ray"], "sources": ["ray:s528944bdec23", "ray:s4796e2ca38da", "ray:s1d86b2f31870"], "status": "REASONED"}, - "agent-placement": {"text": "Worker agents accept cluster submissions with their own managers; drivers normally use the head unless resources, labels or worker-placement override permit workers.", "components": ["ray"], "sources": ["ray:sa7d4f677aadb", "ray:sa5873642c8a2", "ray:s2d562f0e7567"], "status": "REASONED"}, - "agent-logs": {"text": "Agent static /logs exposes the node log directory and index.", "components": ["ray"], "sources": ["ray:s9976ecf5b1f1"], "status": "REASONED"}, - "agent-http-auth": {"text": "Without token mode a browser heuristic is the only filter; token mode guards all agent HTTP paths except two health endpoints.", "components": ["ray"], "sources": ["ray:s17bc66b5543e", "ray:sc96f32b8a459", "ray:sbca5ef4de0bb", "ray:scdfff3ba79f6"], "status": "REASONED"}, - "agent-grpc": {"text": "Agent gRPC defaults OS-assigned with --dashboard-agent-grpc-port override; localhost or wildcard bind serves log/profiling RPCs with optional token interception.", "components": ["ray"], "sources": ["ray:sb1935b32e677", "ray:sa542601368d1", "ray:sd12bacfbc3c7", "ray:s7ef1edbcda5c", "ray:s6f5310eb313d", "ray:s8f508c7e2c94", "ray:s532a98227d6d"], "status": "REASONED"}, - "runtime-agent": {"text": "Runtime-env HTTP defaults OS-assigned on node IP with --runtime-env-agent-port override; create/delete installs packages, and token middleware exempts no route.", "components": ["ray"], "sources": ["ray:s532a98227d6d", "ray:s434c7b4364dc", "ray:s08aa128e5d70", "ray:sb1935b32e677"], "status": "REASONED"}, - "metrics-agent": {"text": "Metrics default enabled; non-minimal reporter uses an OS-assigned port or --metrics-export-port, outside all token middleware.", "components": ["ray"], "sources": ["ray:s532a98227d6d", "ray:s7f27125f73b7", "ray:sf921a5d6a898", "ray:s883e3e00e4df", "ray:sdb1c4564137b", "ray:se9af8ec90932"], "status": "REASONED"}, - "metrics-ipv4": {"text": "WSGI metrics uses AF_INET: empty address is IPv4 wildcard, localhost is used for local node addresses, and resolved ::1 cannot bind this socket.", "components": ["ray", "python"], "sources": ["ray:s7f27125f73b7", "ray:sf921a5d6a898", "ray:s3a079f7e3d82", "python:sdd5a8e2ab59c", "python:se4933ce3da65", "python:s0020052d7267"], "status": "REASONED"}, - "node-normalization": {"text": "Both CLI and ray.init normalize localhost node arguments to a discovered address; cluster-mode setting changes whole-node selection, not independent agent binds.", "components": ["ray"], "sources": ["ray:s7451c8e00be3", "ray:s6c7faadf88a4", "ray:s3625ec280c5e", "ray:sd3a18ab2a112"], "status": "REASONED"}, - "agent-family": {"text": "Agent gRPC wildcard selection is 0.0.0.0 or :: based on resolved localhost, independently of node address family.", "components": ["ray"], "sources": ["ray:sdd8960062a9c", "ray:s51476b743c19"], "status": "REASONED"}, - "gcs-not-redis": {"text": "6379 is GCS, not Redis; Redis stopped being launched by default in 1.11.", "components": ["redis-history", "docs"], "sources": ["redis-history:sb5afda59324d", "docs:s56c7c98ab722"], "status": "REASONED"}, - "redis-backend": {"text": "External Redis is opt-in GCS fault-tolerance storage; Redis credentials do not authenticate GCS clients and RAY_REDIS_PASSWORD is not a head flag substitute.", "components": ["docs"], "sources": ["docs:s56c7c98ab722", "docs:s54ed4b890b65"], "status": "REASONED"}, - "token-default": {"text": "Shared token authentication exists since 2.52.0 but is disabled by default as of 2.58.0; it supplements network isolation and does not protect Serve apps.", "components": ["token-min", "ray"], "sources": ["token-min:sa5e2c2d58c12", "ray:sed3f197cc1ef"], "status": "REASONED"}, - "token-input": {"text": "Enable RAY_AUTH_MODE=token; precedence is RAY_AUTH_TOKEN, RAY_AUTH_TOKEN_PATH then ~/.ray/auth_token, shared by every node/client.", "components": ["ray"], "sources": ["ray:sed3f197cc1ef"], "status": "REASONED"}, - "token-storage": {"text": "Generate/protect token files and copy before startup; tokens are plaintext, do not expire and must never be committed.", "components": ["ray"], "sources": ["ray:sed3f197cc1ef"], "status": "REASONED"}, - "token-transport": {"text": "Token HTTP headers require a trusted tunnel or TLS; plaintext HTTP exposes them on the network.", "components": ["docs", "ray"], "sources": ["docs:s6f0aa11f549a", "ray:sed3f197cc1ef"], "status": "REASONED"}, - "kuberay-token": {"text": "KubeRay v1.6.0 defines authOptions; v1.5.0 RayClusterSpec lacks it. With token mode enabled, the controller creates a random-token Secret when Kubernetes ServiceAccount token authentication is off, SecretName is null or empty, and the generated-name Secret does not already exist; it leaves an existing generated-name Secret or a supplied named Secret in place and creates no Secret in ServiceAccount mode. The pod builder sets RAY_AUTH_MODE when unset on head and worker Ray containers, on the wait-gcs-ready init container when present, and on the head autoscaler when autoscaling is enabled; outside ServiceAccount mode it adds RAY_AUTH_TOKEN from the selected Secret when unset, and in ServiceAccount mode it does not add RAY_AUTH_TOKEN. This guide infers support in v1.6.0 and later from the compared tags; later releases have not been checked.", "components": ["kuberay-auth", "kuberay-auth-source", "kuberay-auth-prior"], "sources": ["kuberay-auth:saf5247262bc7", "kuberay-auth-source:s9a9f9edcc07e", "kuberay-auth-source:s51c84c0bc3b4", "kuberay-auth-source:s61e750cdc2e0", "kuberay-auth-source:s6171707b3edb", "kuberay-auth-source:s408a3a202322", "kuberay-auth-source:s8505add03e5a", "kuberay-auth-source:s1cb4f972131a", "kuberay-auth-source:sd1b672bc4edd", "kuberay-auth-source:se37635162dc9", "kuberay-auth-prior:s04c5ce0b599d"], "status": "REASONED"}, - "mfa": {"text": "Ray has no user accounts; MFA belongs on SSH, tailnet or identity-aware proxy access.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12"], "status": "REASONED"}, - "grpc-tls": {"text": "RAY_USE_TLS defaults 0; export it and server certificate/key/CA variables on every node before startup for internal gRPC mutual TLS.", "components": ["docs"], "sources": ["docs:s261570b2672b"], "status": "REASONED"}, - "tls-scope": {"text": "gRPC TLS can cost performance and does not replace isolation or the dashboard/Jobs HTTP TLS proxy.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12", "docs:s261570b2672b"], "status": "REASONED"}, - "runtime-code": {"text": "Job runtime_env installs before entrypoint; ray.init environments apply to child tasks/actors. Packages, archives, py_executable and setup hooks execute submitted code.", "components": ["docs"], "sources": ["docs:sc375ff2ca01f"], "status": "REASONED"}, - "runtime-policy": {"text": "No built-in package/URI/hook admission policy; eager_install and setup_timeout_seconds control timing, not trust. Prebuild reviewed dependencies.", "components": ["docs"], "sources": ["docs:sc375ff2ca01f", "docs:sb7a1d0b7dfb3"], "status": "REASONED"}, - "serve-bind": {"text": "Serve HTTP defaults port 8000, Python host 127.0.0.1 but config-file host 0.0.0.0; EveryNode puts proxies on nodes with replicas.", "components": ["docs"], "sources": ["docs:scd3f7d8a9b02", "docs:s15aa6ec80fb8", "docs:s045d75fbdecc"], "status": "REASONED"}, - "serve-auth": {"text": "Serve apps have no built-in application auth and are outside cluster token mode; restrict origins and add proxy or FastAPI authentication.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12", "docs:sc98a37c2b157"], "status": "REASONED"}, - "serve-tls": {"text": "Serve ssl_keyfile/ssl_certfile/ssl_ca_certs default None; supplying a CA does not itself require client certificates.", "components": ["docs"], "sources": ["docs:scd3f7d8a9b02"], "status": "REASONED"}, - "serve-grpc": {"text": "Optional Serve gRPC uses 9000 only with configured servicer functions.", "components": ["docs", "ray"], "sources": ["docs:s045d75fbdecc", "ray:scc7af6be456e"], "status": "REASONED"}, - "image-privilege": {"text": "Official image ray UID 1000/GID 100 has passwordless sudo; non-root startup alone does not prevent root escalation.", "components": ["ray"], "sources": ["ray:s08d31327265a"], "status": "REASONED"}, - "pod-security": {"text": "KubeRay v1.7.0 head/worker pod/container contexts default empty; enforce non-root UID/GID, no privilege escalation, dropped capabilities and RuntimeDefault seccomp.", "components": ["kuberay", "kubernetes"], "sources": ["kuberay:s76e45858aa01", "kubernetes:sb78ea91d0308"], "status": "REASONED"}, - "verify-inventory": {"text": "Inventory every head/worker and actual randomized agent ports after restarts; expected missing listeners need explanation. No isolated multi-node cluster was available.", "components": ["ray"], "sources": ["ray:s9a060fe08a53", "ray:s6f5310eb313d", "ray:s532a98227d6d", "ray:s7f27125f73b7", "ray:s434c7b4364dc"], "status": "REASONED", "verify": [1]}, - "verify-external": {"text": "Probe every routed IPv4/IPv6 origin and inventoried port; completed TCP, even HTTP 401 or non-HTTP, proves exposure. Resolver/local errors are inconclusive.", "components": ["docs", "curl"], "sources": ["docs:sa5e2c2d58c12", "docs:s261570b2672b", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, - "verify-jobs": {"text": "Through the trusted tunnel valid-token submission must succeed and token-free submission must fail authentication with 401, not a connection error.", "components": ["ray", "kuberay-auth"], "sources": ["ray:sed3f197cc1ef", "kuberay-auth:saf5247262bc7"], "status": "REASONED", "verify": [1]}, - "verify-grpc-tls": {"text": "ray health-check must pass with the correct CA and fail certificate verification with a wrong CA/name; local config errors are inconclusive.", "components": ["docs"], "sources": ["docs:s261570b2672b"], "status": "REASONED", "verify": [1]}, - "verify-agent": {"text": "Direct non-browser /logs/ must expose an index with token mode off, allow a valid token with it on and reject absent tokens with 401; health exemptions cannot discriminate.", "components": ["ray"], "sources": ["ray:s9976ecf5b1f1", "ray:s17bc66b5543e", "ray:sbca5ef4de0bb", "ray:sefae1a66c0dd"], "status": "REASONED", "verify": [2]}, - "verify-token-secret": {"text": "Trusted-path hidden prompt and stdin header avoid new token argv/history exposure and set +x avoids tracing; account owner/root can still read memory.", "components": ["ray", "curl"], "sources": ["ray:sed3f197cc1ef", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]} - } -} ---- # Ray: dashboard, Jobs, and Client ports execute code - -**Version basis** +Written against Ray pinned source ray-2.58.0, Ray token authentication introduction 2.52.0, KubeRay chart v1.7.0, KubeRay authentication source v1.6.0, KubeRay prior RayCluster types v1.5.0, CPython WSGI server v3.11.0, and curl minimum write-out version 7.75.0; 1 further source is pinned to a commit and 5 carry no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 46 claims: 1 demonstrated by a check that ran and 45 reasoned from cited sources. -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. -Metadata reviewed 2026-09-26; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +Ray's own security page is blunt: if you expose the Ray Dashboard, Ray Jobs, or Ray Client services, "anybody who can access the associated ports can execute arbitrary code on your Ray Cluster", explicitly by submitting a Job or connecting a Client, indirectly through the Dashboard REST API, and implicitly because Ray deserializes arbitrary Python objects with cloudpickle. Ray "doesn't implement access controls for developers interacting with a given cluster"; security and isolation "must be enforced outside of the Ray Cluster". The head ports in question are the dashboard (and Jobs API) on `8265`, the Ray Client server on `10001`, and the head node port `6379`, all plain HTTP or gRPC with no login of their own unless you enable token authentication (step 3). The per-node agent listeners in step 2 add independent paths to job submission, logs, profiling, runtime environments and metrics. -Components (recorded basis): +## 1. Keep the dashboard on loopback -- ray: Ray pinned source (ray-2.58.0) -- docs: Ray documentation (unknown) -- token-min: Ray token authentication introduction (2.52.0) -- redis-history: Ray Redis history announcement (unknown) -- docker: Docker publishing documentation (unknown) -- kuberay-auth: KubeRay authentication documentation (unknown) -- kuberay: KubeRay chart (v1.7.0) -- kubernetes: Kubernetes security contexts (unknown) -- python: CPython WSGI server (v3.11.0) -- curl: curl minimum write-out version (7.75.0) -- docker-release: Docker Engine release notes (4e9a5751518ed8223a8dcde53693badddd72604f) -- kuberay-auth-source: KubeRay authentication source (v1.6.0) -- kuberay-auth-prior: KubeRay prior RayCluster types (v1.5.0) +`ray start --dashboard-host` defaults to localhost (as of Ray 2.58.0, the first address `localhost` resolves to, IPv4 before IPv6, else `127.0.0.1`), and `--dashboard-port` defaults to `8265`. Leave both alone and say so explicitly, because most tutorials and container entrypoints override the host to make the UI reachable: -Claims (full source mapping in the front matter): - -- boundary (REASONED; docs): Dashboard, Jobs and Client access permits arbitrary code execution; use an isolated cluster network and separate clusters for mutually untrusted jobs. -- dashboard (REASONED; ray): Dashboard defaults to resolved localhost, IPv4 before IPv6 then 127.0.0.1, on port 8265; state loopback explicitly. -- docker (REASONED; docker, docker-release): Bridge publishing uses host loopback and container-interface bind; this guide assesses Docker 28.0.0 or later with normal NAT as the loopback-isolation minimum because the 28.0.0 release notes record a fix for neighbour hosts reaching loopback-mapped ports. -- access (REASONED; docs, kuberay-auth): Reach Dashboard/Jobs through loopback SSH forwarding, launcher forwarding, KubeRay forwarding or a private tailnet; public browser access needs an authenticating TLS proxy. -- head-ports (REASONED; docs): Head GCS uses 6379 and Client 10001; worker ports default 10002-19999 plus randomized listeners. -- core-bind (REASONED; ray): Core gRPC source binds all interfaces unless node address is exactly 127.0.0.1, ::1 or localhost; private node IP does not imply private bind. -- gcs-observation (DEMONSTRATED; ray): Four attempted Ray 2.58.0 loopback starts still showed GCS *:6379; the cause was not established and the watcher stopped the runs. -- client-bind (REASONED; ray): Ray Client server/proxier listen on the supplied node address plus loopback; restrict access and prefer Jobs over publishing Client. -- serve-replica (REASONED; ray): Serve inter-deployment gRPC binds [::] on an OS-chosen port; this is source reasoning, not a run. -- agents (REASONED; ray): --include-dashboard=false does not stop dashboard/runtime-env agents; head dashboard itself is reduced to usage stats. -- minimal (REASONED; ray): Missing optional imports select minimal mode: no agent HTTP/gRPC or metrics reporter; runtime-env HTTP remains. Base installs may have full dependencies. -- agent-http (REASONED; ray): Non-minimal agent HTTP defaults 52365, including ray.init; --dashboard-agent-listen-port changes it, with node-IP plus conditional localhost binds. -- agent-jobs (REASONED; ray): Direct /api/job_agent/jobs/ submission and stop/delete/log routes bypass a proxy on 8265; head forwarding reaches its agent job manager. -- agent-placement (REASONED; ray): Worker agents accept cluster submissions with their own managers; drivers normally use the head unless resources, labels or worker-placement override permit workers. -- agent-logs (REASONED; ray): Agent static /logs exposes the node log directory and index. -- agent-http-auth (REASONED; ray): Without token mode a browser heuristic is the only filter; token mode guards all agent HTTP paths except two health endpoints. -- agent-grpc (REASONED; ray): Agent gRPC defaults OS-assigned with --dashboard-agent-grpc-port override; localhost or wildcard bind serves log/profiling RPCs with optional token interception. -- runtime-agent (REASONED; ray): Runtime-env HTTP defaults OS-assigned on node IP with --runtime-env-agent-port override; create/delete installs packages, and token middleware exempts no route. -- metrics-agent (REASONED; ray): Metrics default enabled; non-minimal reporter uses an OS-assigned port or --metrics-export-port, outside all token middleware. -- metrics-ipv4 (REASONED; ray, python): WSGI metrics uses AF_INET: empty address is IPv4 wildcard, localhost is used for local node addresses, and resolved ::1 cannot bind this socket. -- node-normalization (REASONED; ray): Both CLI and ray.init normalize localhost node arguments to a discovered address; cluster-mode setting changes whole-node selection, not independent agent binds. -- agent-family (REASONED; ray): Agent gRPC wildcard selection is 0.0.0.0 or :: based on resolved localhost, independently of node address family. -- gcs-not-redis (REASONED; redis-history, docs): 6379 is GCS, not Redis; Redis stopped being launched by default in 1.11. -- redis-backend (REASONED; docs): External Redis is opt-in GCS fault-tolerance storage; Redis credentials do not authenticate GCS clients and RAY_REDIS_PASSWORD is not a head flag substitute. -- token-default (REASONED; token-min, ray): Shared token authentication exists since 2.52.0 but is disabled by default as of 2.58.0; it supplements network isolation and does not protect Serve apps. -- token-input (REASONED; ray): Enable RAY_AUTH_MODE=token; precedence is RAY_AUTH_TOKEN, RAY_AUTH_TOKEN_PATH then ~/.ray/auth_token, shared by every node/client. -- token-storage (REASONED; ray): Generate/protect token files and copy before startup; tokens are plaintext, do not expire and must never be committed. -- token-transport (REASONED; docs, ray): Token HTTP headers require a trusted tunnel or TLS; plaintext HTTP exposes them on the network. -- kuberay-token (REASONED; kuberay-auth, kuberay-auth-source, kuberay-auth-prior): KubeRay v1.6.0 defines authOptions; v1.5.0 RayClusterSpec lacks it. With token mode enabled, the controller creates a random-token Secret when Kubernetes ServiceAccount token authentication is off, SecretName is null or empty, and the generated-name Secret does not already exist; it leaves an existing generated-name Secret or a supplied named Secret in place and creates no Secret in ServiceAccount mode. The pod builder sets RAY_AUTH_MODE when unset on head and worker Ray containers, on the wait-gcs-ready init container when present, and on the head autoscaler when autoscaling is enabled; outside ServiceAccount mode it adds RAY_AUTH_TOKEN from the selected Secret when unset, and in ServiceAccount mode it does not add RAY_AUTH_TOKEN. This guide infers support in v1.6.0 and later from the compared tags; later releases have not been checked. -- mfa (REASONED; docs): Ray has no user accounts; MFA belongs on SSH, tailnet or identity-aware proxy access. -- grpc-tls (REASONED; docs): RAY_USE_TLS defaults 0; export it and server certificate/key/CA variables on every node before startup for internal gRPC mutual TLS. -- tls-scope (REASONED; docs): gRPC TLS can cost performance and does not replace isolation or the dashboard/Jobs HTTP TLS proxy. -- runtime-code (REASONED; docs): Job runtime_env installs before entrypoint; ray.init environments apply to child tasks/actors. Packages, archives, py_executable and setup hooks execute submitted code. -- runtime-policy (REASONED; docs): No built-in package/URI/hook admission policy; eager_install and setup_timeout_seconds control timing, not trust. Prebuild reviewed dependencies. -- serve-bind (REASONED; docs): Serve HTTP defaults port 8000, Python host 127.0.0.1 but config-file host 0.0.0.0; EveryNode puts proxies on nodes with replicas. -- serve-auth (REASONED; docs): Serve apps have no built-in application auth and are outside cluster token mode; restrict origins and add proxy or FastAPI authentication. -- serve-tls (REASONED; docs): Serve ssl_keyfile/ssl_certfile/ssl_ca_certs default None; supplying a CA does not itself require client certificates. -- serve-grpc (REASONED; docs, ray): Optional Serve gRPC uses 9000 only with configured servicer functions. -- image-privilege (REASONED; ray): Official image ray UID 1000/GID 100 has passwordless sudo; non-root startup alone does not prevent root escalation. -- pod-security (REASONED; kuberay, kubernetes): KubeRay v1.7.0 head/worker pod/container contexts default empty; enforce non-root UID/GID, no privilege escalation, dropped capabilities and RuntimeDefault seccomp. -- verify-inventory (REASONED; ray): Inventory every head/worker and actual randomized agent ports after restarts; expected missing listeners need explanation. No isolated multi-node cluster was available. -- verify-external (REASONED; docs, curl): Probe every routed IPv4/IPv6 origin and inventoried port; completed TCP, even HTTP 401 or non-HTTP, proves exposure. Resolver/local errors are inconclusive. -- verify-jobs (REASONED; ray, kuberay-auth): Through the trusted tunnel valid-token submission must succeed and token-free submission must fail authentication with 401, not a connection error. -- verify-grpc-tls (REASONED; docs): ray health-check must pass with the correct CA and fail certificate verification with a wrong CA/name; local config errors are inconclusive. -- verify-agent (REASONED; ray): Direct non-browser /logs/ must expose an index with token mode off, allow a valid token with it on and reject absent tokens with 401; health exemptions cannot discriminate. -- verify-token-secret (REASONED; ray, curl): Trusted-path hidden prompt and stdin header avoid new token argv/history exposure and set +x avoids tracing; account owner/root can still read memory. - - -Ray's own security page is blunt: if you expose the Ray Dashboard, Ray Jobs, or Ray Client services, "anybody who can access the associated ports can execute arbitrary code on your Ray Cluster", explicitly by submitting a Job or connecting a Client, indirectly through the Dashboard REST API, and implicitly because Ray deserializes arbitrary Python objects with cloudpickle. Ray "doesn't implement access controls for developers interacting with a given cluster"; security and isolation "must be enforced outside of the Ray Cluster". The head ports in question are the dashboard (and Jobs API) on `8265`, the Ray Client server on `10001`, and the head node port `6379`, all plain HTTP or gRPC with no login of their own unless you enable token authentication (step 3). The per-node agent listeners in step 2 add independent paths to job submission, logs, profiling, runtime environments and metrics. - -## 1. Keep the dashboard on loopback - -`ray start --dashboard-host` defaults to localhost (as of Ray 2.58.0, the first address `localhost` resolves to, IPv4 before IPv6, else `127.0.0.1`), and `--dashboard-port` defaults to `8265`. Leave both alone and say so explicitly, because most tutorials and container entrypoints override the host to make the UI reachable: - -```bash -ray start --head --dashboard-host 127.0.0.1 --dashboard-port 8265 -``` +```bash +ray start --head --dashboard-host 127.0.0.1 --dashboard-port 8265 +``` Never pass `--dashboard-host 0.0.0.0` (or `::`) on a machine with a public interface. In Docker the two binds are different things: the host publishes on loopback only (`-p 127.0.0.1:8265:8265`, which Docker's port-publishing docs say only the Docker host can reach, assuming a normal bridge-network NAT setup and a Docker version at or after 28.0.0; the [Docker 28.0.0 release notes](https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/release-notes/28.md#L768-L768) record a fix for neighbour hosts reaching loopback-mapped ports, so this guide assesses earlier releases as potentially allowing hosts on the same layer-2 network to reach a loopback-published port), while inside the container the dashboard must listen on the container's own interface (`--dashboard-host 0.0.0.0` there, private to the Compose network and the host), because a dashboard bound to the container's `127.0.0.1` is not reachable through the published port at all. See [docker.md](docker.md). @@ -598,3 +299,328 @@ REASONED: mixed historical listener observations and unrun service comparisons f - Kubernetes security contexts (runAsNonRoot, drop capabilities, seccomp): https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ - KubeRay ray-cluster Helm chart values (head/worker podSecurityContext and securityContext, default {}): https://github.com/ray-project/kuberay/blob/v1.7.0/helm-chart/ray-cluster/values.yaml - [Ray ray-2.58.0 Serve gRPC default port and servicer-function requirement (checked October 2026)](https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/serve/config.py#L955-L963). + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/ray.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- ray: Ray pinned source (ray-2.58.0) +- token-min: Ray token authentication introduction (2.52.0) +- docs: Ray documentation (unknown) +- redis-history: Ray Redis history announcement (unknown) +- kuberay-auth: KubeRay authentication documentation (unknown) +- kuberay: KubeRay chart (v1.7.0) +- kuberay-auth-source: KubeRay authentication source (v1.6.0) +- kuberay-auth-prior: KubeRay prior RayCluster types (v1.5.0) +- docker: Docker publishing documentation (unknown) +- kubernetes: Kubernetes security contexts (unknown) +- docker-release: Docker Engine release notes (4e9a5751518ed8223a8dcde53693badddd72604f) +- python: CPython WSGI server (v3.11.0) +- curl: curl minimum write-out version (7.75.0) + +Claims (full source mapping in the JSON block below): + +- boundary (REASONED; docs): Dashboard, Jobs and Client access permits arbitrary code execution; use an isolated cluster network and separate clusters for mutually untrusted jobs. +- dashboard (REASONED; ray): Dashboard defaults to resolved localhost, IPv4 before IPv6 then 127.0.0.1, on port 8265; state loopback explicitly. +- docker (REASONED; docker, docker-release): Bridge publishing uses host loopback and container-interface bind; this guide assesses Docker 28.0.0 or later with normal NAT as the loopback-isolation minimum because the 28.0.0 release notes record a fix for neighbour hosts reaching loopback-mapped ports. +- access (REASONED; docs, kuberay-auth): Reach Dashboard/Jobs through loopback SSH forwarding, launcher forwarding, KubeRay forwarding or a private tailnet; public browser access needs an authenticating TLS proxy. +- head-ports (REASONED; docs): Head GCS uses 6379 and Client 10001; worker ports default 10002-19999 plus randomized listeners. +- core-bind (REASONED; ray): Core gRPC source binds all interfaces unless node address is exactly 127.0.0.1, ::1 or localhost; private node IP does not imply private bind. +- gcs-observation (DEMONSTRATED; ray): Four attempted Ray 2.58.0 loopback starts still showed GCS *:6379; the cause was not established and the watcher stopped the runs. +- client-bind (REASONED; ray): Ray Client server/proxier listen on the supplied node address plus loopback; restrict access and prefer Jobs over publishing Client. +- serve-replica (REASONED; ray): Serve inter-deployment gRPC binds [::] on an OS-chosen port; this is source reasoning, not a run. +- agents (REASONED; ray): --include-dashboard=false does not stop dashboard/runtime-env agents; head dashboard itself is reduced to usage stats. +- minimal (REASONED; ray): Missing optional imports select minimal mode: no agent HTTP/gRPC or metrics reporter; runtime-env HTTP remains. Base installs may have full dependencies. +- agent-http (REASONED; ray): Non-minimal agent HTTP defaults 52365, including ray.init; --dashboard-agent-listen-port changes it, with node-IP plus conditional localhost binds. +- agent-jobs (REASONED; ray): Direct /api/job_agent/jobs/ submission and stop/delete/log routes bypass a proxy on 8265; head forwarding reaches its agent job manager. +- agent-placement (REASONED; ray): Worker agents accept cluster submissions with their own managers; drivers normally use the head unless resources, labels or worker-placement override permit workers. +- agent-logs (REASONED; ray): Agent static /logs exposes the node log directory and index. +- agent-http-auth (REASONED; ray): Without token mode a browser heuristic is the only filter; token mode guards all agent HTTP paths except two health endpoints. +- agent-grpc (REASONED; ray): Agent gRPC defaults OS-assigned with --dashboard-agent-grpc-port override; localhost or wildcard bind serves log/profiling RPCs with optional token interception. +- runtime-agent (REASONED; ray): Runtime-env HTTP defaults OS-assigned on node IP with --runtime-env-agent-port override; create/delete installs packages, and token middleware exempts no route. +- metrics-agent (REASONED; ray): Metrics default enabled; non-minimal reporter uses an OS-assigned port or --metrics-export-port, outside all token middleware. +- metrics-ipv4 (REASONED; ray, python): WSGI metrics uses AF_INET: empty address is IPv4 wildcard, localhost is used for local node addresses, and resolved ::1 cannot bind this socket. +- node-normalization (REASONED; ray): Both CLI and ray.init normalize localhost node arguments to a discovered address; cluster-mode setting changes whole-node selection, not independent agent binds. +- agent-family (REASONED; ray): Agent gRPC wildcard selection is 0.0.0.0 or :: based on resolved localhost, independently of node address family. +- gcs-not-redis (REASONED; redis-history, docs): 6379 is GCS, not Redis; Redis stopped being launched by default in 1.11. +- redis-backend (REASONED; docs): External Redis is opt-in GCS fault-tolerance storage; Redis credentials do not authenticate GCS clients and RAY_REDIS_PASSWORD is not a head flag substitute. +- token-default (REASONED; token-min, ray): Shared token authentication exists since 2.52.0 but is disabled by default as of 2.58.0; it supplements network isolation and does not protect Serve apps. +- token-input (REASONED; ray): Enable RAY_AUTH_MODE=token; precedence is RAY_AUTH_TOKEN, RAY_AUTH_TOKEN_PATH then ~/.ray/auth_token, shared by every node/client. +- token-storage (REASONED; ray): Generate/protect token files and copy before startup; tokens are plaintext, do not expire and must never be committed. +- token-transport (REASONED; docs, ray): Token HTTP headers require a trusted tunnel or TLS; plaintext HTTP exposes them on the network. +- kuberay-token (REASONED; kuberay-auth, kuberay-auth-source, kuberay-auth-prior): KubeRay v1.6.0 defines authOptions; v1.5.0 RayClusterSpec lacks it. With token mode enabled, the controller creates a random-token Secret when Kubernetes ServiceAccount token authentication is off, SecretName is null or empty, and the generated-name Secret does not already exist; it leaves an existing generated-name Secret or a supplied named Secret in place and creates no Secret in ServiceAccount mode. The pod builder sets RAY_AUTH_MODE when unset on head and worker Ray containers, on the wait-gcs-ready init container when present, and on the head autoscaler when autoscaling is enabled; outside ServiceAccount mode it adds RAY_AUTH_TOKEN from the selected Secret when unset, and in ServiceAccount mode it does not add RAY_AUTH_TOKEN. This guide infers support in v1.6.0 and later from the compared tags; later releases have not been checked. +- mfa (REASONED; docs): Ray has no user accounts; MFA belongs on SSH, tailnet or identity-aware proxy access. +- grpc-tls (REASONED; docs): RAY_USE_TLS defaults 0; export it and server certificate/key/CA variables on every node before startup for internal gRPC mutual TLS. +- tls-scope (REASONED; docs): gRPC TLS can cost performance and does not replace isolation or the dashboard/Jobs HTTP TLS proxy. +- runtime-code (REASONED; docs): Job runtime_env installs before entrypoint; ray.init environments apply to child tasks/actors. Packages, archives, py_executable and setup hooks execute submitted code. +- runtime-policy (REASONED; docs): No built-in package/URI/hook admission policy; eager_install and setup_timeout_seconds control timing, not trust. Prebuild reviewed dependencies. +- serve-bind (REASONED; docs): Serve HTTP defaults port 8000, Python host 127.0.0.1 but config-file host 0.0.0.0; EveryNode puts proxies on nodes with replicas. +- serve-auth (REASONED; docs): Serve apps have no built-in application auth and are outside cluster token mode; restrict origins and add proxy or FastAPI authentication. +- serve-tls (REASONED; docs): Serve ssl_keyfile/ssl_certfile/ssl_ca_certs default None; supplying a CA does not itself require client certificates. +- serve-grpc (REASONED; docs, ray): Optional Serve gRPC uses 9000 only with configured servicer functions. +- image-privilege (REASONED; ray): Official image ray UID 1000/GID 100 has passwordless sudo; non-root startup alone does not prevent root escalation. +- pod-security (REASONED; kuberay, kubernetes): KubeRay v1.7.0 head/worker pod/container contexts default empty; enforce non-root UID/GID, no privilege escalation, dropped capabilities and RuntimeDefault seccomp. +- verify-inventory (REASONED; ray): Inventory every head/worker and actual randomized agent ports after restarts; expected missing listeners need explanation. No isolated multi-node cluster was available. +- verify-external (REASONED; docs, curl): Probe every routed IPv4/IPv6 origin and inventoried port; completed TCP, even HTTP 401 or non-HTTP, proves exposure. Resolver/local errors are inconclusive. +- verify-jobs (REASONED; ray, kuberay-auth): Through the trusted tunnel valid-token submission must succeed and token-free submission must fail authentication with 401, not a connection error. +- verify-grpc-tls (REASONED; docs): ray health-check must pass with the correct CA and fail certificate verification with a wrong CA/name; local config errors are inconclusive. +- verify-agent (REASONED; ray): Direct non-browser /logs/ must expose an index with token mode off, allow a valid token with it on and reject absent tokens with 401; health exemptions cannot discriminate. +- verify-token-secret (REASONED; ray, curl): Trusted-path hidden prompt and stdin header avoid new token argv/history exposure and set +x avoids tracing; account owner/root can still read memory. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "286cb3508e87ad0658bd57c73270ab3b4100681250bae339f4d71a573e537bfb", + "components": { + "ray": { + "name": "Ray pinned source", + "basis": "ray-2.58.0", + "sources": { + "sed3f197cc1ef": "https://github.com/ray-project/ray/blob/ray-2.58.0/doc/source/ray-security/token-auth.md", + "s08d31327265a": "https://raw.githubusercontent.com/ray-project/ray/ray-2.58.0/docker/base-deps/Dockerfile", + "s2740a1880e2b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/rpc/grpc_server.cc#L67-L68", + "s958054ab801b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.h#L111-L113", + "sa3d2966f536b": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L601-L617", + "s050b349f5bbd": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L185", + "sd7d2d0d963d0": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.cc#L257-L278", + "s329ab4e0ed63": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/serve/_private/replica.py#L1778", + "s3eb2c2cb2cbb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/util/client/server/server.py#L799-L801", + "sae1bb5af6e35": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/util/client/server/proxier.py#L929-L931", + "s207ba00c4e86": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1572-L1575", + "s3efda42d7913": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L2464-L2471", + "s9a060fe08a53": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L189", + "sb1935b32e677": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L618-L635", + "se9af8ec90932": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L687-L692", + "s17bc66b5543e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L20-L23", + "s7df4fce455ca": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L54-L66", + "sc96f32b8a459": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L107-L113", + "sbca5ef4de0bb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/authentication/http_token_authentication.py#L28-L80", + "scdfff3ba79f6": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/optional_utils.py#L132-L209", + "s528944bdec23": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_agent.py#L32-L196", + "s4796e2ca38da": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_head.py#L123-L144", + "s1d86b2f31870": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_head.py#L267-L280", + "s9976ecf5b1f1": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L245-L249", + "sa542601368d1": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L264-L307", + "sd12bacfbc3c7": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L345-L414", + "s7ef1edbcda5c": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L570-L638", + "s6f5310eb313d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L144-L169", + "s8f508c7e2c94": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L182-L187", + "s48b3469099a3": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L100-L117", + "s532a98227d6d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L372-L377", + "s33a5ff7c987b": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1660-L1669", + "s1a19fd793b8f": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1768", + "s8f1ebe4bd911": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1381-L1387", + "s7682a13e83a2": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1862-L1883", + "s9237e74ea054": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1918-L1929", + "s4ce6fe077ba9": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L2022-L2031", + "s7f27125f73b7": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L504-L516", + "sf921a5d6a898": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/prometheus_exporter.py#L326-L334", + "s434c7b4364dc": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L218-L224", + "s08aa128e5d70": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L243-L248", + "sdd8960062a9c": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/includes/network_util.pxi#L103-L110", + "s51476b743c19": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.cc#L280-L289", + "s7451c8e00be3": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L850-L854", + "s6c7faadf88a4": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L786-L821", + "s3625ec280c5e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/worker.py#L1835-L1836", + "sd3a18ab2a112": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L507-L514", + "sba3f67581471": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/parameter.py#L165-L167", + "s994d0ddb6a7d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/worker.py#L1884-L1912", + "sd4d44da36278": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/utils.py#L1128-L1142", + "s5a9d9c171f49": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1909-L1913", + "s9cd95d66d32a": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/utils.py#L322-L351", + "s762672f680e9": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L2099-L2101", + "s28e3a1076d01": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L23-L33", + "s883e3e00e4df": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/common/ray_config_def.h#L618-L619", + "sdb1c4564137b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/raylet/node_manager.cc#L3527-L3530", + "sa7d4f677aadb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_agent.py#L198-L203", + "sa5873642c8a2": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_manager.py#L421-L463", + "s2d562f0e7567": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_manager.py#L565-L609", + "s3a079f7e3d82": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/prometheus_exporter.py#L10", + "sefae1a66c0dd": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L102-L114", + "scc7af6be456e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/serve/config.py#L955-L963" + } + }, + "token-min": { + "name": "Ray token authentication introduction", + "basis": "2.52.0", + "sources": { + "sa5e2c2d58c12": "https://docs.ray.io/en/latest/ray-security/index.html" + } + }, + "docs": { + "name": "Ray documentation", + "basis": "unknown", + "sources": { + "sa5e2c2d58c12": "https://docs.ray.io/en/latest/ray-security/index.html", + "s6f0aa11f549a": "https://docs.ray.io/en/latest/ray-security/token-auth.html", + "s56c7c98ab722": "https://docs.ray.io/en/latest/cluster/cli.html", + "s261570b2672b": "https://docs.ray.io/en/latest/ray-core/configure.html", + "sc375ff2ca01f": "https://docs.ray.io/en/latest/ray-core/handling-dependencies.html", + "sb7a1d0b7dfb3": "https://docs.ray.io/en/latest/ray-core/api/doc/ray.runtime_env.RuntimeEnv.html", + "scd3f7d8a9b02": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.config.HTTPOptions.html", + "s15aa6ec80fb8": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.schema.HTTPOptionsSchema.html", + "s045d75fbdecc": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.schema.ServeDeploySchema.html", + "sc98a37c2b157": "https://docs.ray.io/en/latest/serve/http-guide.html", + "s54ed4b890b65": "https://docs.ray.io/en/latest/cluster/kubernetes/user-guides/kuberay-gcs-ft.html" + } + }, + "redis-history": { + "name": "Ray Redis history announcement", + "basis": "unknown", + "sources": { + "sb5afda59324d": "https://www.anyscale.com/blog/redis-in-ray-past-and-future" + } + }, + "kuberay-auth": { + "name": "KubeRay authentication documentation", + "basis": "unknown", + "sources": { + "saf5247262bc7": "https://docs.ray.io/en/latest/cluster/kubernetes/user-guides/kuberay-auth.html" + } + }, + "kuberay": { + "name": "KubeRay chart", + "basis": "v1.7.0", + "sources": { + "s76e45858aa01": "https://github.com/ray-project/kuberay/blob/v1.7.0/helm-chart/ray-cluster/values.yaml" + } + }, + "kuberay-auth-source": { + "name": "KubeRay authentication source", + "basis": "v1.6.0", + "sources": { + "s9a9f9edcc07e": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/apis/ray/v1/raycluster_types.go#L17-L19", + "s51c84c0bc3b4": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/apis/ray/v1/raycluster_types.go#L81-L107", + "s61e750cdc2e0": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/raycluster_controller.go#L369-L433", + "s6171707b3edb": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L194-L220", + "s408a3a202322": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L234-L236", + "s8505add03e5a": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L253-L271", + "s1cb4f972131a": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L296-L336", + "sd1b672bc4edd": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L460-L462", + "se37635162dc9": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/utils/util.go#L760-L766" + } + }, + "kuberay-auth-prior": { + "name": "KubeRay prior RayCluster types", + "basis": "v1.5.0", + "sources": { + "s04c5ce0b599d": "https://github.com/ray-project/kuberay/blob/v1.5.0/ray-operator/apis/ray/v1/raycluster_types.go#L12-L47" + } + }, + "docker": { + "name": "Docker publishing documentation", + "basis": "unknown", + "sources": { + "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" + } + }, + "kubernetes": { + "name": "Kubernetes security contexts", + "basis": "unknown", + "sources": { + "sb78ea91d0308": "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/" + } + }, + "docker-release": { + "name": "Docker Engine release notes", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "sd733f57ded23": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/release-notes/28.md#L768-L768" + } + }, + "python": { + "name": "CPython WSGI server", + "basis": "v3.11.0", + "sources": { + "sdd5a8e2ab59c": "https://github.com/python/cpython/blob/v3.11.0/Lib/wsgiref/simple_server.py#L1-L160", + "se4933ce3da65": "https://github.com/python/cpython/blob/v3.11.0/Lib/http/server.py#L120-L145", + "s0020052d7267": "https://github.com/python/cpython/blob/v3.11.0/Lib/socketserver.py#L400-L480" + } + }, + "curl": { + "name": "curl minimum write-out version", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + } + }, + "claims": { + "boundary": {"text": "Dashboard, Jobs and Client access permits arbitrary code execution; use an isolated cluster network and separate clusters for mutually untrusted jobs.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12"], "status": "REASONED"}, + "dashboard": {"text": "Dashboard defaults to resolved localhost, IPv4 before IPv6 then 127.0.0.1, on port 8265; state loopback explicitly.", "components": ["ray"], "sources": ["ray:sa3d2966f536b", "ray:s050b349f5bbd", "ray:sd7d2d0d963d0"], "status": "REASONED"}, + "docker": {"text": "Bridge publishing uses host loopback and container-interface bind; this guide assesses Docker 28.0.0 or later with normal NAT as the loopback-isolation minimum because the 28.0.0 release notes record a fix for neighbour hosts reaching loopback-mapped ports.", "components": ["docker", "docker-release"], "sources": ["docker:s1e53417c513d", "docker-release:sd733f57ded23"], "status": "REASONED"}, + "access": {"text": "Reach Dashboard/Jobs through loopback SSH forwarding, launcher forwarding, KubeRay forwarding or a private tailnet; public browser access needs an authenticating TLS proxy.", "components": ["docs", "kuberay-auth"], "sources": ["docs:sa5e2c2d58c12", "docs:s56c7c98ab722", "kuberay-auth:saf5247262bc7"], "status": "REASONED"}, + "head-ports": {"text": "Head GCS uses 6379 and Client 10001; worker ports default 10002-19999 plus randomized listeners.", "components": ["docs"], "sources": ["docs:s56c7c98ab722", "docs:s261570b2672b"], "status": "REASONED"}, + "core-bind": {"text": "Core gRPC source binds all interfaces unless node address is exactly 127.0.0.1, ::1 or localhost; private node IP does not imply private bind.", "components": ["ray"], "sources": ["ray:s2740a1880e2b", "ray:s958054ab801b"], "status": "REASONED"}, + "gcs-observation": {"text": "Four attempted Ray 2.58.0 loopback starts still showed GCS *:6379; the cause was not established and the watcher stopped the runs.", "components": ["ray"], "sources": ["ray:s2740a1880e2b", "ray:s958054ab801b"], "status": "DEMONSTRATED", "evidence": "in each of four runs using `ray start --head --node-ip-address=127.0.0.1 --port=6379 --dashboard-host=127.0.0.1`, `ss` still showed the GCS listener as `*:6379`."}, + "client-bind": {"text": "Ray Client server/proxier listen on the supplied node address plus loopback; restrict access and prefer Jobs over publishing Client.", "components": ["ray"], "sources": ["ray:s3eb2c2cb2cbb", "ray:sae1bb5af6e35", "ray:s207ba00c4e86", "ray:s3efda42d7913"], "status": "REASONED"}, + "serve-replica": {"text": "Serve inter-deployment gRPC binds [::] on an OS-chosen port; this is source reasoning, not a run.", "components": ["ray"], "sources": ["ray:s329ab4e0ed63"], "status": "REASONED"}, + "agents": {"text": "--include-dashboard=false does not stop dashboard/runtime-env agents; head dashboard itself is reduced to usage stats.", "components": ["ray"], "sources": ["ray:s33a5ff7c987b", "ray:s1a19fd793b8f", "ray:s8f1ebe4bd911", "ray:s7682a13e83a2", "ray:s9237e74ea054", "ray:s4ce6fe077ba9"], "status": "REASONED"}, + "minimal": {"text": "Missing optional imports select minimal mode: no agent HTTP/gRPC or metrics reporter; runtime-env HTTP remains. Base installs may have full dependencies.", "components": ["ray"], "sources": ["ray:s48b3469099a3", "ray:sd4d44da36278", "ray:s5a9d9c171f49", "ray:s9cd95d66d32a", "ray:s762672f680e9", "ray:s28e3a1076d01"], "status": "REASONED"}, + "agent-http": {"text": "Non-minimal agent HTTP defaults 52365, including ray.init; --dashboard-agent-listen-port changes it, with node-IP plus conditional localhost binds.", "components": ["ray"], "sources": ["ray:s9a060fe08a53", "ray:sb1935b32e677", "ray:s7df4fce455ca", "ray:sba3f67581471", "ray:s994d0ddb6a7d"], "status": "REASONED"}, + "agent-jobs": {"text": "Direct /api/job_agent/jobs/ submission and stop/delete/log routes bypass a proxy on 8265; head forwarding reaches its agent job manager.", "components": ["ray"], "sources": ["ray:s528944bdec23", "ray:s4796e2ca38da", "ray:s1d86b2f31870"], "status": "REASONED"}, + "agent-placement": {"text": "Worker agents accept cluster submissions with their own managers; drivers normally use the head unless resources, labels or worker-placement override permit workers.", "components": ["ray"], "sources": ["ray:sa7d4f677aadb", "ray:sa5873642c8a2", "ray:s2d562f0e7567"], "status": "REASONED"}, + "agent-logs": {"text": "Agent static /logs exposes the node log directory and index.", "components": ["ray"], "sources": ["ray:s9976ecf5b1f1"], "status": "REASONED"}, + "agent-http-auth": {"text": "Without token mode a browser heuristic is the only filter; token mode guards all agent HTTP paths except two health endpoints.", "components": ["ray"], "sources": ["ray:s17bc66b5543e", "ray:sc96f32b8a459", "ray:sbca5ef4de0bb", "ray:scdfff3ba79f6"], "status": "REASONED"}, + "agent-grpc": {"text": "Agent gRPC defaults OS-assigned with --dashboard-agent-grpc-port override; localhost or wildcard bind serves log/profiling RPCs with optional token interception.", "components": ["ray"], "sources": ["ray:sb1935b32e677", "ray:sa542601368d1", "ray:sd12bacfbc3c7", "ray:s7ef1edbcda5c", "ray:s6f5310eb313d", "ray:s8f508c7e2c94", "ray:s532a98227d6d"], "status": "REASONED"}, + "runtime-agent": {"text": "Runtime-env HTTP defaults OS-assigned on node IP with --runtime-env-agent-port override; create/delete installs packages, and token middleware exempts no route.", "components": ["ray"], "sources": ["ray:s532a98227d6d", "ray:s434c7b4364dc", "ray:s08aa128e5d70", "ray:sb1935b32e677"], "status": "REASONED"}, + "metrics-agent": {"text": "Metrics default enabled; non-minimal reporter uses an OS-assigned port or --metrics-export-port, outside all token middleware.", "components": ["ray"], "sources": ["ray:s532a98227d6d", "ray:s7f27125f73b7", "ray:sf921a5d6a898", "ray:s883e3e00e4df", "ray:sdb1c4564137b", "ray:se9af8ec90932"], "status": "REASONED"}, + "metrics-ipv4": {"text": "WSGI metrics uses AF_INET: empty address is IPv4 wildcard, localhost is used for local node addresses, and resolved ::1 cannot bind this socket.", "components": ["ray", "python"], "sources": ["ray:s7f27125f73b7", "ray:sf921a5d6a898", "ray:s3a079f7e3d82", "python:sdd5a8e2ab59c", "python:se4933ce3da65", "python:s0020052d7267"], "status": "REASONED"}, + "node-normalization": {"text": "Both CLI and ray.init normalize localhost node arguments to a discovered address; cluster-mode setting changes whole-node selection, not independent agent binds.", "components": ["ray"], "sources": ["ray:s7451c8e00be3", "ray:s6c7faadf88a4", "ray:s3625ec280c5e", "ray:sd3a18ab2a112"], "status": "REASONED"}, + "agent-family": {"text": "Agent gRPC wildcard selection is 0.0.0.0 or :: based on resolved localhost, independently of node address family.", "components": ["ray"], "sources": ["ray:sdd8960062a9c", "ray:s51476b743c19"], "status": "REASONED"}, + "gcs-not-redis": {"text": "6379 is GCS, not Redis; Redis stopped being launched by default in 1.11.", "components": ["redis-history", "docs"], "sources": ["redis-history:sb5afda59324d", "docs:s56c7c98ab722"], "status": "REASONED"}, + "redis-backend": {"text": "External Redis is opt-in GCS fault-tolerance storage; Redis credentials do not authenticate GCS clients and RAY_REDIS_PASSWORD is not a head flag substitute.", "components": ["docs"], "sources": ["docs:s56c7c98ab722", "docs:s54ed4b890b65"], "status": "REASONED"}, + "token-default": {"text": "Shared token authentication exists since 2.52.0 but is disabled by default as of 2.58.0; it supplements network isolation and does not protect Serve apps.", "components": ["token-min", "ray"], "sources": ["token-min:sa5e2c2d58c12", "ray:sed3f197cc1ef"], "status": "REASONED"}, + "token-input": {"text": "Enable RAY_AUTH_MODE=token; precedence is RAY_AUTH_TOKEN, RAY_AUTH_TOKEN_PATH then ~/.ray/auth_token, shared by every node/client.", "components": ["ray"], "sources": ["ray:sed3f197cc1ef"], "status": "REASONED"}, + "token-storage": {"text": "Generate/protect token files and copy before startup; tokens are plaintext, do not expire and must never be committed.", "components": ["ray"], "sources": ["ray:sed3f197cc1ef"], "status": "REASONED"}, + "token-transport": {"text": "Token HTTP headers require a trusted tunnel or TLS; plaintext HTTP exposes them on the network.", "components": ["docs", "ray"], "sources": ["docs:s6f0aa11f549a", "ray:sed3f197cc1ef"], "status": "REASONED"}, + "kuberay-token": {"text": "KubeRay v1.6.0 defines authOptions; v1.5.0 RayClusterSpec lacks it. With token mode enabled, the controller creates a random-token Secret when Kubernetes ServiceAccount token authentication is off, SecretName is null or empty, and the generated-name Secret does not already exist; it leaves an existing generated-name Secret or a supplied named Secret in place and creates no Secret in ServiceAccount mode. The pod builder sets RAY_AUTH_MODE when unset on head and worker Ray containers, on the wait-gcs-ready init container when present, and on the head autoscaler when autoscaling is enabled; outside ServiceAccount mode it adds RAY_AUTH_TOKEN from the selected Secret when unset, and in ServiceAccount mode it does not add RAY_AUTH_TOKEN. This guide infers support in v1.6.0 and later from the compared tags; later releases have not been checked.", "components": ["kuberay-auth", "kuberay-auth-source", "kuberay-auth-prior"], "sources": ["kuberay-auth:saf5247262bc7", "kuberay-auth-source:s9a9f9edcc07e", "kuberay-auth-source:s51c84c0bc3b4", "kuberay-auth-source:s61e750cdc2e0", "kuberay-auth-source:s6171707b3edb", "kuberay-auth-source:s408a3a202322", "kuberay-auth-source:s8505add03e5a", "kuberay-auth-source:s1cb4f972131a", "kuberay-auth-source:sd1b672bc4edd", "kuberay-auth-source:se37635162dc9", "kuberay-auth-prior:s04c5ce0b599d"], "status": "REASONED"}, + "mfa": {"text": "Ray has no user accounts; MFA belongs on SSH, tailnet or identity-aware proxy access.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12"], "status": "REASONED"}, + "grpc-tls": {"text": "RAY_USE_TLS defaults 0; export it and server certificate/key/CA variables on every node before startup for internal gRPC mutual TLS.", "components": ["docs"], "sources": ["docs:s261570b2672b"], "status": "REASONED"}, + "tls-scope": {"text": "gRPC TLS can cost performance and does not replace isolation or the dashboard/Jobs HTTP TLS proxy.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12", "docs:s261570b2672b"], "status": "REASONED"}, + "runtime-code": {"text": "Job runtime_env installs before entrypoint; ray.init environments apply to child tasks/actors. Packages, archives, py_executable and setup hooks execute submitted code.", "components": ["docs"], "sources": ["docs:sc375ff2ca01f"], "status": "REASONED"}, + "runtime-policy": {"text": "No built-in package/URI/hook admission policy; eager_install and setup_timeout_seconds control timing, not trust. Prebuild reviewed dependencies.", "components": ["docs"], "sources": ["docs:sc375ff2ca01f", "docs:sb7a1d0b7dfb3"], "status": "REASONED"}, + "serve-bind": {"text": "Serve HTTP defaults port 8000, Python host 127.0.0.1 but config-file host 0.0.0.0; EveryNode puts proxies on nodes with replicas.", "components": ["docs"], "sources": ["docs:scd3f7d8a9b02", "docs:s15aa6ec80fb8", "docs:s045d75fbdecc"], "status": "REASONED"}, + "serve-auth": {"text": "Serve apps have no built-in application auth and are outside cluster token mode; restrict origins and add proxy or FastAPI authentication.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12", "docs:sc98a37c2b157"], "status": "REASONED"}, + "serve-tls": {"text": "Serve ssl_keyfile/ssl_certfile/ssl_ca_certs default None; supplying a CA does not itself require client certificates.", "components": ["docs"], "sources": ["docs:scd3f7d8a9b02"], "status": "REASONED"}, + "serve-grpc": {"text": "Optional Serve gRPC uses 9000 only with configured servicer functions.", "components": ["docs", "ray"], "sources": ["docs:s045d75fbdecc", "ray:scc7af6be456e"], "status": "REASONED"}, + "image-privilege": {"text": "Official image ray UID 1000/GID 100 has passwordless sudo; non-root startup alone does not prevent root escalation.", "components": ["ray"], "sources": ["ray:s08d31327265a"], "status": "REASONED"}, + "pod-security": {"text": "KubeRay v1.7.0 head/worker pod/container contexts default empty; enforce non-root UID/GID, no privilege escalation, dropped capabilities and RuntimeDefault seccomp.", "components": ["kuberay", "kubernetes"], "sources": ["kuberay:s76e45858aa01", "kubernetes:sb78ea91d0308"], "status": "REASONED"}, + "verify-inventory": {"text": "Inventory every head/worker and actual randomized agent ports after restarts; expected missing listeners need explanation. No isolated multi-node cluster was available.", "components": ["ray"], "sources": ["ray:s9a060fe08a53", "ray:s6f5310eb313d", "ray:s532a98227d6d", "ray:s7f27125f73b7", "ray:s434c7b4364dc"], "status": "REASONED", "verify": [1]}, + "verify-external": {"text": "Probe every routed IPv4/IPv6 origin and inventoried port; completed TCP, even HTTP 401 or non-HTTP, proves exposure. Resolver/local errors are inconclusive.", "components": ["docs", "curl"], "sources": ["docs:sa5e2c2d58c12", "docs:s261570b2672b", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, + "verify-jobs": {"text": "Through the trusted tunnel valid-token submission must succeed and token-free submission must fail authentication with 401, not a connection error.", "components": ["ray", "kuberay-auth"], "sources": ["ray:sed3f197cc1ef", "kuberay-auth:saf5247262bc7"], "status": "REASONED", "verify": [1]}, + "verify-grpc-tls": {"text": "ray health-check must pass with the correct CA and fail certificate verification with a wrong CA/name; local config errors are inconclusive.", "components": ["docs"], "sources": ["docs:s261570b2672b"], "status": "REASONED", "verify": [1]}, + "verify-agent": {"text": "Direct non-browser /logs/ must expose an index with token mode off, allow a valid token with it on and reject absent tokens with 401; health exemptions cannot discriminate.", "components": ["ray"], "sources": ["ray:s9976ecf5b1f1", "ray:s17bc66b5543e", "ray:sbca5ef4de0bb", "ray:sefae1a66c0dd"], "status": "REASONED", "verify": [2]}, + "verify-token-secret": {"text": "Trusted-path hidden prompt and stdin header avoid new token argv/history exposure and set +x avoids tracing; account owner/root can still read memory.", "components": ["ray", "curl"], "sources": ["ray:sed3f197cc1ef", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]} + } +} +```` + diff --git a/realtime-webhooks.md b/realtime-webhooks.md index cd87fb2f4..5823d7062 100644 --- a/realtime-webhooks.md +++ b/realtime-webhooks.md @@ -1,6 +1,6 @@ # WebSockets, server-sent events, and webhooks: authenticating the non-page endpoints -Written against WebSocket protocol RFC 6455, stripe-node v18.5.0, stripe-python v12.5.0, Traefik timeout reference v3.5, Caddy source v2.11.4, nginx source release-1.26.3, HAProxy 3.0, and HAProxy source v3.0.0, plus 1 more recorded bases; 7 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 36 claims, all reasoned from cited sources. +Written against WebSocket protocol RFC 6455, stripe-node v18.5.0, stripe-python v12.5.0, Traefik timeout reference v3.5, Caddy source v2.11.4, nginx source release-1.26.3, HAProxy 3.0, and HAProxy source v3.0.0, plus 1 more recorded basis; 7 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 36 claims, all reasoned from cited sources. A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. diff --git a/self-hosted-ci-runners.md b/self-hosted-ci-runners.md index ec98ac060..fc8863802 100644 --- a/self-hosted-ci-runners.md +++ b/self-hosted-ci-runners.md @@ -1,167 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-12", - "documentation_checked": "2026-09", - "body_sha256": "72e6c5e6e8aa98cd95989f98eb383a327daaecd20809fcb2d24aea181dee4f4e", - "components": { - "github": { - "name": "GitHub Actions documentation", - "basis": "unknown", - "sources": { - "s215dc07e3642": "https://docs.github.com/en/actions/reference/security/secure-use", - "s92e8e4820392": "https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/manage-access", - "sbbcb15f795a8": "https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows", - "s4561d20e7042": "https://docs.github.com/en/actions/concepts/security/github_token", - "s80fe11d8c90d": "https://docs.github.com/en/actions/reference/security/oidc" - } - }, - "github-api": { - "name": "GitHub REST API", - "basis": "2026-03-10", - "sources": { - "s8217448953a4": "https://docs.github.com/en/rest/actions/self-hosted-runners", - "sd36db9ea1031": "https://docs.github.com/en/rest/actions/self-hosted-runner-groups", - "sbeee4a47110c": "https://docs.github.com/en/rest/actions/workflow-jobs" - } - }, - "gitlab": { - "name": "GitLab rolling documentation", - "basis": "unknown", - "sources": { - "seb30e3937183": "https://docs.gitlab.com/runner/security/", - "sde661ff4762e": "https://docs.gitlab.com/runner/executors/docker_autoscaler/", - "s265254bc5592": "https://docs.gitlab.com/runner/executors/docker/", - "s47b236d63dc4": "https://docs.gitlab.com/runner/configuration/advanced-configuration/", - "s1579c499c75b": "https://docs.gitlab.com/runner/executors/kubernetes/", - "sd2cbff5d5d6e": "https://docs.gitlab.com/ci/runners/new_creation_workflow/", - "se430cbabc011": "https://docs.gitlab.com/api/users/#create-a-runner-linked-to-a-user", - "saef0483b7be7": "https://docs.gitlab.com/ci/jobs/ci_job_token/", - "s699c1c198119": "https://docs.gitlab.com/api/project_job_token_scopes/", - "sbc009ff2866a": "https://docs.gitlab.com/runner/monitoring/" - } - }, - "python": { - "name": "Python tomllib minimum", - "basis": "3.11", - "sources": { - "s37be9e98ea29": "https://docs.python.org/3.11/library/tomllib.html" - } - }, - "docker": { - "name": "Docker Engine documentation", - "basis": "unknown", - "sources": { - "s18f59818e451": "https://docs.docker.com/engine/security/" - } - }, - "aws": { - "name": "AWS EC2 documentation", - "basis": "unknown", - "sources": { - "s82aca706445c": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html" - } - } - }, - "claims": { - "job-transport": {"text": "Both runners poll over HTTPS without an inbound job-polling port; verify GitLab TLS and restrict separate autoscaler management or webhook listeners.", "components": ["github", "gitlab"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "gitlab:sde661ff4762e"], "status": "REASONED"}, - "worker-account": {"text": "Use a dedicated least-privileged account, keep provisioning credentials off workers and confine rootful Docker access to disposable VMs.", "components": ["github", "gitlab", "docker"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "docker:s18f59818e451"], "status": "REASONED"}, - "worker-egress": {"text": "Enforce job egress outside job control, isolate production and other workers, and deny unused metadata over both families; IMDSv2 does not stop arbitrary authorized job code.", "components": ["github", "gitlab", "aws"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "aws:s82aca706445c"], "status": "REASONED"}, - "github-eligibility": {"text": "Keep self-hosted runners off public repositories; runner groups admit only private repositories by default and the public override should stay off.", "components": ["github"], "sources": ["github:s92e8e4820392", "github:s215dc07e3642"], "status": "REASONED"}, - "fork-token": {"text": "Ordinary fork pull_request jobs normally receive a read-only GITHUB_TOKEN and no other secrets, but can still read host credentials and reachable services.", "components": ["github"], "sources": ["github:s215dc07e3642", "github:sbbcb15f795a8"], "status": "REASONED"}, - "privileged-triggers": {"text": "pull_request_target and workflow_run can access secrets and write tokens; never execute untrusted PR code, artifacts or caches in these jobs.", "components": ["github"], "sources": ["github:sbbcb15f795a8", "github:s215dc07e3642"], "status": "REASONED"}, - "private-forks": {"text": "Review private-repository fork settings, which can grant write tokens and secrets; separate trusted privileged workers from untrusted CI.", "components": ["github"], "sources": ["github:s215dc07e3642"], "status": "REASONED"}, - "registration-token": {"text": "Registration tokens last one hour; generate on demand, never bake into images, and do not confuse expiry with runner deregistration.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, - "registration-manager": {"text": "Keep the App/PAT generating registration or JIT credentials off workers; organization endpoints need Self-hosted runners: write, repository endpoints Administration: write.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, - "jit": {"text": "Protect encoded_jit_config and the whole response; JIT runners execute at most one job.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, - "runner-credential": {"text": "Protect the installed runner authentication credential, directory and backups.", "components": ["github"], "sources": ["github:s215dc07e3642"], "status": "REASONED"}, - "github-job-token": {"text": "GITHUB_TOKEN is repository-scoped and per-job; set read-only defaults and grant only trusted jobs additional permissions.", "components": ["github"], "sources": ["github:s4561d20e7042"], "status": "REASONED"}, - "oidc": {"text": "Grant id-token: write only where needed; the cloud trust must restrict issuer, audience and repository/branch or protected-environment subject.", "components": ["github"], "sources": ["github:s80fe11d8c90d"], "status": "REASONED"}, - "github-lifecycle": {"text": "Prefer ephemeral/JIT workers and destroy their machines and storage; deregistration does not destroy disks, and labels route rather than authorize jobs.", "components": ["github", "github-api"], "sources": ["github:s215dc07e3642", "github:s92e8e4820392", "github-api:s8217448953a4"], "status": "REASONED"}, - "shell-executor": {"text": "The GitLab shell executor runs as the runner user without isolation; avoid it for untrusted jobs on shared hosts.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183"], "status": "REASONED"}, - "docker-executor": {"text": "Use unprivileged Docker; privileged=true or services_privileged and host Docker socket/TCP access can expose the host despite privileged=false.", "components": ["gitlab", "docker"], "sources": ["gitlab:s265254bc5592", "gitlab:seb30e3937183", "docker:s18f59818e451"], "status": "REASONED"}, - "executor-host-access": {"text": "Deny sensitive host mounts, host PID/network namespaces, unnecessary devices and capabilities; build without the host daemon.", "components": ["gitlab"], "sources": ["gitlab:s265254bc5592", "gitlab:s47b236d63dc4"], "status": "REASONED"}, - "kubernetes-executor": {"text": "Enforce nonprivileged containers, no privilege escalation, scoped ServiceAccounts and no unnecessary token mounting; prevent job overrides.", "components": ["gitlab"], "sources": ["gitlab:s1579c499c75b"], "status": "REASONED"}, - "gitlab-disposal": {"text": "Containers share the kernel; use disposable separate VMs/nodes as needed and one-job autoscaling workers. Instance and Docker Autoscaler GA is recorded as Runner 17.1.", "components": ["gitlab"], "sources": ["gitlab:sde661ff4762e", "gitlab:seb30e3937183"], "status": "REASONED"}, - "gitlab-registration": {"text": "Legacy registration tokens were disabled by default in GitLab 17.0, with removal scheduled for 20.0; use runner authentication tokens.", "components": ["gitlab"], "sources": ["gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, - "gitlab-runner-token": {"text": "Protect and rotate the runner authentication token in config.toml and backups; a cloned runner can steal assigned jobs.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, - "gitlab-creation": {"text": "POST /user/runners needs create_runner scope; keep that access token off workers and set scope/protection at creation.", "components": ["gitlab"], "sources": ["gitlab:se430cbabc011", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, - "gitlab-job-token": {"text": "CI_JOB_TOKEN carries supported triggering-user access during the job and appears as CI_REGISTRY_PASSWORD and inside CI_REPOSITORY_URL.", "components": ["gitlab"], "sources": ["gitlab:saef0483b7be7"], "status": "REASONED"}, - "gitlab-allowlist": {"text": "Target-project inbound job-token allowlists limit who may access that target, not job egress; also restrict resource feature visibility.", "components": ["gitlab"], "sources": ["gitlab:saef0483b7be7", "gitlab:s699c1c198119"], "status": "REASONED"}, - "gitlab-forks": {"text": "Instance runners on public installations accept strangers; project scope does not establish code trust. Review parent-launched fork pipelines or disable ci_allow_fork_pipelines_to_run_in_parent_project.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183"], "status": "REASONED"}, - "gitlab-protected": {"text": "Developer access can compromise persistent runners; release runners need ref_protected and a pool separate from fork-merge-request testing.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, - "gitlab-metrics": {"text": "Optional listen_address metrics and profiling have no built-in authorization; 9252 is allocated, not proof of the configured port.", "components": ["gitlab"], "sources": ["gitlab:sbc009ff2866a"], "status": "REASONED"}, - "gitlab-session": {"text": "Optional session_server uses TLS and must admit required GitLab traffic at its configured address; 8093 is an example, not an established default.", "components": ["gitlab"], "sources": ["gitlab:s47b236d63dc4"], "status": "REASONED"}, - "verify-eligibility": {"text": "Fully paginate groups, selected repositories and public repositories' own runners; visibility=all alone is not public eligibility, and inherited access needs review.", "components": ["github-api"], "sources": ["github-api:sd36db9ea1031", "github-api:s8217448953a4"], "status": "REASONED", "verify": [1]}, - "verify-permissions": {"text": "Inspect organization and repository workflow-token defaults and PR approval settings; explicit workflow permissions can raise grants and id-token is separate.", "components": ["github"], "sources": ["github:s4561d20e7042", "github:s80fe11d8c90d"], "status": "REASONED", "verify": [2]}, - "verify-assignment": {"text": "A controlled fork workflow should run when exposed and lose assignment when restricted, while an allowed private job still runs; skipped/pending/offline/timeout is inconclusive.", "components": ["github-api", "github"], "sources": ["github-api:sbeee4a47110c", "github:s92e8e4820392"], "status": "REASONED", "verify": [3]}, - "verify-config": {"text": "Python 3.14.4 synthetic fixtures reported exposed/fixed Docker, Kubernetes and autoscaler values; wrong types passed through, misplaced keys printed null, and empty/error cases differed in output and exit status.", "components": ["python", "gitlab"], "sources": ["python:s37be9e98ea29", "gitlab:s47b236d63dc4", "gitlab:s1579c499c75b"], "status": "DEMONSTRATED", "verify": [4], "evidence": "the extracted Python program ran without sudo on 20 synthetic config.toml cases using Python 3.14.4. Docker fixtures printed true/true, a socket mount and a TCP host versus false/false, empty volumes and a null host; both exited 0. Autoscaler limits printed null/null, 1/2 and 2/1 versus 1/1, all exit 0. Kubernetes fixtures printed true versus false for the three booleans and the supplied service_account. Wrong-type strings, booleans and floats retained their JSON types; misplaced privileged printed docker.privileged null. Empty and runner-less files printed nothing and exited 0. Missing files, malformed TOML and wrong section types exited 1; the second-runner structure error followed one printed runner. Two valid runners printed indices 0 and 1. The runner token key was omitted; a dummy token copied into service_account was printed. These observations cover file inspection only."}, - "verify-daemon": {"text": "Resolve docker.host through DOCKER_HOST or the default socket when absent; flag actual job access to a rootful daemon, not tcp:// alone.", "components": ["gitlab", "docker"], "sources": ["gitlab:s265254bc5592", "docker:s18f59818e451"], "status": "REASONED"}, - "verify-autoscaler": {"text": "For Instance/Docker Autoscaler require capacity_per_instance=1 and max_use_count=1 explicitly as integers; absent or other values are findings.", "components": ["gitlab"], "sources": ["gitlab:sde661ff4762e"], "status": "REASONED"}, - "verify-gitlab-api": {"text": "Read inbound allowlists and require release-runner ref_protected against exposed controls; gitlab-runner verify proves authentication only.", "components": ["gitlab"], "sources": ["gitlab:s699c1c198119", "gitlab:seb30e3937183"], "status": "REASONED", "verify": [5]}, - "verify-lifecycle": {"text": "Check complete deregistration listings and infrastructure disposal; persistent-worker canary must survive in the control, but absence alone does not prove destruction or clean shared caches.", "components": ["github-api", "gitlab", "github"], "sources": ["github-api:s8217448953a4", "gitlab:sde661ff4762e", "github:s215dc07e3642"], "status": "REASONED"}, - "verify-listeners": {"text": "Inventory namespaces and publications, require the permitted-client response, then test disallowed access; any HTTP response proves reachability, failure needs corroborating firewall denial.", "components": ["gitlab"], "sources": ["gitlab:sbc009ff2866a", "gitlab:s47b236d63dc4"], "status": "REASONED", "verify": [6]} - } -} ---- # Self-hosted CI runners: GitHub Actions and GitLab Runner - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-12; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- github: GitHub Actions documentation (unknown) -- github-api: GitHub REST API (2026-03-10) -- gitlab: GitLab rolling documentation (unknown) -- python: Python tomllib minimum (3.11) -- docker: Docker Engine documentation (unknown) -- aws: AWS EC2 documentation (unknown) +Written against GitHub Actions documentation (no recorded version), GitHub REST API 2026-03-10, and Python tomllib minimum 3.11; 3 further sources carry no recorded version. Metadata reviewed 2026-10-12; documentation checked September 2026. The guide records 37 claims: 1 demonstrated by a check that ran and 36 reasoned from cited sources. -Claims (full source mapping in the front matter): - -- job-transport (REASONED; github, gitlab): Both runners poll over HTTPS without an inbound job-polling port; verify GitLab TLS and restrict separate autoscaler management or webhook listeners. -- worker-account (REASONED; github, gitlab, docker): Use a dedicated least-privileged account, keep provisioning credentials off workers and confine rootful Docker access to disposable VMs. -- worker-egress (REASONED; github, gitlab, aws): Enforce job egress outside job control, isolate production and other workers, and deny unused metadata over both families; IMDSv2 does not stop arbitrary authorized job code. -- github-eligibility (REASONED; github): Keep self-hosted runners off public repositories; runner groups admit only private repositories by default and the public override should stay off. -- fork-token (REASONED; github): Ordinary fork pull_request jobs normally receive a read-only GITHUB_TOKEN and no other secrets, but can still read host credentials and reachable services. -- privileged-triggers (REASONED; github): pull_request_target and workflow_run can access secrets and write tokens; never execute untrusted PR code, artifacts or caches in these jobs. -- private-forks (REASONED; github): Review private-repository fork settings, which can grant write tokens and secrets; separate trusted privileged workers from untrusted CI. -- registration-token (REASONED; github-api): Registration tokens last one hour; generate on demand, never bake into images, and do not confuse expiry with runner deregistration. -- registration-manager (REASONED; github-api): Keep the App/PAT generating registration or JIT credentials off workers; organization endpoints need Self-hosted runners: write, repository endpoints Administration: write. -- jit (REASONED; github-api): Protect encoded_jit_config and the whole response; JIT runners execute at most one job. -- runner-credential (REASONED; github): Protect the installed runner authentication credential, directory and backups. -- github-job-token (REASONED; github): GITHUB_TOKEN is repository-scoped and per-job; set read-only defaults and grant only trusted jobs additional permissions. -- oidc (REASONED; github): Grant id-token: write only where needed; the cloud trust must restrict issuer, audience and repository/branch or protected-environment subject. -- github-lifecycle (REASONED; github, github-api): Prefer ephemeral/JIT workers and destroy their machines and storage; deregistration does not destroy disks, and labels route rather than authorize jobs. -- shell-executor (REASONED; gitlab): The GitLab shell executor runs as the runner user without isolation; avoid it for untrusted jobs on shared hosts. -- docker-executor (REASONED; gitlab, docker): Use unprivileged Docker; privileged=true or services_privileged and host Docker socket/TCP access can expose the host despite privileged=false. -- executor-host-access (REASONED; gitlab): Deny sensitive host mounts, host PID/network namespaces, unnecessary devices and capabilities; build without the host daemon. -- kubernetes-executor (REASONED; gitlab): Enforce nonprivileged containers, no privilege escalation, scoped ServiceAccounts and no unnecessary token mounting; prevent job overrides. -- gitlab-disposal (REASONED; gitlab): Containers share the kernel; use disposable separate VMs/nodes as needed and one-job autoscaling workers. Instance and Docker Autoscaler GA is recorded as Runner 17.1. -- gitlab-registration (REASONED; gitlab): Legacy registration tokens were disabled by default in GitLab 17.0, with removal scheduled for 20.0; use runner authentication tokens. -- gitlab-runner-token (REASONED; gitlab): Protect and rotate the runner authentication token in config.toml and backups; a cloned runner can steal assigned jobs. -- gitlab-creation (REASONED; gitlab): POST /user/runners needs create_runner scope; keep that access token off workers and set scope/protection at creation. -- gitlab-job-token (REASONED; gitlab): CI_JOB_TOKEN carries supported triggering-user access during the job and appears as CI_REGISTRY_PASSWORD and inside CI_REPOSITORY_URL. -- gitlab-allowlist (REASONED; gitlab): Target-project inbound job-token allowlists limit who may access that target, not job egress; also restrict resource feature visibility. -- gitlab-forks (REASONED; gitlab): Instance runners on public installations accept strangers; project scope does not establish code trust. Review parent-launched fork pipelines or disable ci_allow_fork_pipelines_to_run_in_parent_project. -- gitlab-protected (REASONED; gitlab): Developer access can compromise persistent runners; release runners need ref_protected and a pool separate from fork-merge-request testing. -- gitlab-metrics (REASONED; gitlab): Optional listen_address metrics and profiling have no built-in authorization; 9252 is allocated, not proof of the configured port. -- gitlab-session (REASONED; gitlab): Optional session_server uses TLS and must admit required GitLab traffic at its configured address; 8093 is an example, not an established default. -- verify-eligibility (REASONED; github-api): Fully paginate groups, selected repositories and public repositories' own runners; visibility=all alone is not public eligibility, and inherited access needs review. -- verify-permissions (REASONED; github): Inspect organization and repository workflow-token defaults and PR approval settings; explicit workflow permissions can raise grants and id-token is separate. -- verify-assignment (REASONED; github-api, github): A controlled fork workflow should run when exposed and lose assignment when restricted, while an allowed private job still runs; skipped/pending/offline/timeout is inconclusive. -- verify-config (DEMONSTRATED; python, gitlab): Python 3.14.4 synthetic fixtures reported exposed/fixed Docker, Kubernetes and autoscaler values; wrong types passed through, misplaced keys printed null, and empty/error cases differed in output and exit status. -- verify-daemon (REASONED; gitlab, docker): Resolve docker.host through DOCKER_HOST or the default socket when absent; flag actual job access to a rootful daemon, not tcp:// alone. -- verify-autoscaler (REASONED; gitlab): For Instance/Docker Autoscaler require capacity_per_instance=1 and max_use_count=1 explicitly as integers; absent or other values are findings. -- verify-gitlab-api (REASONED; gitlab): Read inbound allowlists and require release-runner ref_protected against exposed controls; gitlab-runner verify proves authentication only. -- verify-lifecycle (REASONED; github-api, gitlab, github): Check complete deregistration listings and infrastructure disposal; persistent-worker canary must survive in the control, but absence alone does not prove destruction or clean shared caches. -- verify-listeners (REASONED; gitlab): Inventory namespaces and publications, require the permitted-client response, then test disallowed access; any HTTP response proves reachability, failure needs corroborating firewall denial. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. A self-hosted runner exists to execute CI job commands on your own machine, so the trust question is not a bind address but who can cause code to run on it. Both GitHub Actions runners and GitLab Runner @@ -433,3 +274,188 @@ Version boundary: GitHub.com documentation checked 18 September 2026, with REST - Docker Engine security (rootful daemon, docker group as host administration): https://docs.docker.com/engine/security/ - AWS EC2 instance metadata service (IMDSv2 does not stop authorized in-job requests): https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html - Python tomllib (offline config.toml inspection, Python 3.11+): https://docs.python.org/3.11/library/tomllib.html + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/self-hosted-ci-runners.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- github: GitHub Actions documentation (unknown) +- github-api: GitHub REST API (2026-03-10) +- gitlab: GitLab rolling documentation (unknown) +- docker: Docker Engine documentation (unknown) +- python: Python tomllib minimum (3.11) +- aws: AWS EC2 documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- job-transport (REASONED; github, gitlab): Both runners poll over HTTPS without an inbound job-polling port; verify GitLab TLS and restrict separate autoscaler management or webhook listeners. +- worker-account (REASONED; github, gitlab, docker): Use a dedicated least-privileged account, keep provisioning credentials off workers and confine rootful Docker access to disposable VMs. +- worker-egress (REASONED; github, gitlab, aws): Enforce job egress outside job control, isolate production and other workers, and deny unused metadata over both families; IMDSv2 does not stop arbitrary authorized job code. +- github-eligibility (REASONED; github): Keep self-hosted runners off public repositories; runner groups admit only private repositories by default and the public override should stay off. +- fork-token (REASONED; github): Ordinary fork pull_request jobs normally receive a read-only GITHUB_TOKEN and no other secrets, but can still read host credentials and reachable services. +- privileged-triggers (REASONED; github): pull_request_target and workflow_run can access secrets and write tokens; never execute untrusted PR code, artifacts or caches in these jobs. +- private-forks (REASONED; github): Review private-repository fork settings, which can grant write tokens and secrets; separate trusted privileged workers from untrusted CI. +- registration-token (REASONED; github-api): Registration tokens last one hour; generate on demand, never bake into images, and do not confuse expiry with runner deregistration. +- registration-manager (REASONED; github-api): Keep the App/PAT generating registration or JIT credentials off workers; organization endpoints need Self-hosted runners: write, repository endpoints Administration: write. +- jit (REASONED; github-api): Protect encoded_jit_config and the whole response; JIT runners execute at most one job. +- runner-credential (REASONED; github): Protect the installed runner authentication credential, directory and backups. +- github-job-token (REASONED; github): GITHUB_TOKEN is repository-scoped and per-job; set read-only defaults and grant only trusted jobs additional permissions. +- oidc (REASONED; github): Grant id-token: write only where needed; the cloud trust must restrict issuer, audience and repository/branch or protected-environment subject. +- github-lifecycle (REASONED; github, github-api): Prefer ephemeral/JIT workers and destroy their machines and storage; deregistration does not destroy disks, and labels route rather than authorize jobs. +- shell-executor (REASONED; gitlab): The GitLab shell executor runs as the runner user without isolation; avoid it for untrusted jobs on shared hosts. +- docker-executor (REASONED; gitlab, docker): Use unprivileged Docker; privileged=true or services_privileged and host Docker socket/TCP access can expose the host despite privileged=false. +- executor-host-access (REASONED; gitlab): Deny sensitive host mounts, host PID/network namespaces, unnecessary devices and capabilities; build without the host daemon. +- kubernetes-executor (REASONED; gitlab): Enforce nonprivileged containers, no privilege escalation, scoped ServiceAccounts and no unnecessary token mounting; prevent job overrides. +- gitlab-disposal (REASONED; gitlab): Containers share the kernel; use disposable separate VMs/nodes as needed and one-job autoscaling workers. Instance and Docker Autoscaler GA is recorded as Runner 17.1. +- gitlab-registration (REASONED; gitlab): Legacy registration tokens were disabled by default in GitLab 17.0, with removal scheduled for 20.0; use runner authentication tokens. +- gitlab-runner-token (REASONED; gitlab): Protect and rotate the runner authentication token in config.toml and backups; a cloned runner can steal assigned jobs. +- gitlab-creation (REASONED; gitlab): POST /user/runners needs create_runner scope; keep that access token off workers and set scope/protection at creation. +- gitlab-job-token (REASONED; gitlab): CI_JOB_TOKEN carries supported triggering-user access during the job and appears as CI_REGISTRY_PASSWORD and inside CI_REPOSITORY_URL. +- gitlab-allowlist (REASONED; gitlab): Target-project inbound job-token allowlists limit who may access that target, not job egress; also restrict resource feature visibility. +- gitlab-forks (REASONED; gitlab): Instance runners on public installations accept strangers; project scope does not establish code trust. Review parent-launched fork pipelines or disable ci_allow_fork_pipelines_to_run_in_parent_project. +- gitlab-protected (REASONED; gitlab): Developer access can compromise persistent runners; release runners need ref_protected and a pool separate from fork-merge-request testing. +- gitlab-metrics (REASONED; gitlab): Optional listen_address metrics and profiling have no built-in authorization; 9252 is allocated, not proof of the configured port. +- gitlab-session (REASONED; gitlab): Optional session_server uses TLS and must admit required GitLab traffic at its configured address; 8093 is an example, not an established default. +- verify-eligibility (REASONED; github-api): Fully paginate groups, selected repositories and public repositories' own runners; visibility=all alone is not public eligibility, and inherited access needs review. +- verify-permissions (REASONED; github): Inspect organization and repository workflow-token defaults and PR approval settings; explicit workflow permissions can raise grants and id-token is separate. +- verify-assignment (REASONED; github-api, github): A controlled fork workflow should run when exposed and lose assignment when restricted, while an allowed private job still runs; skipped/pending/offline/timeout is inconclusive. +- verify-config (DEMONSTRATED; python, gitlab): Python 3.14.4 synthetic fixtures reported exposed/fixed Docker, Kubernetes and autoscaler values; wrong types passed through, misplaced keys printed null, and empty/error cases differed in output and exit status. +- verify-daemon (REASONED; gitlab, docker): Resolve docker.host through DOCKER_HOST or the default socket when absent; flag actual job access to a rootful daemon, not tcp:// alone. +- verify-autoscaler (REASONED; gitlab): For Instance/Docker Autoscaler require capacity_per_instance=1 and max_use_count=1 explicitly as integers; absent or other values are findings. +- verify-gitlab-api (REASONED; gitlab): Read inbound allowlists and require release-runner ref_protected against exposed controls; gitlab-runner verify proves authentication only. +- verify-lifecycle (REASONED; github-api, gitlab, github): Check complete deregistration listings and infrastructure disposal; persistent-worker canary must survive in the control, but absence alone does not prove destruction or clean shared caches. +- verify-listeners (REASONED; gitlab): Inventory namespaces and publications, require the permitted-client response, then test disallowed access; any HTTP response proves reachability, failure needs corroborating firewall denial. + +````json +{ + "schema": 1, + "checked": "2026-10-12", + "documentation_checked": "2026-09", + "body_sha256": "72e6c5e6e8aa98cd95989f98eb383a327daaecd20809fcb2d24aea181dee4f4e", + "components": { + "github": { + "name": "GitHub Actions documentation", + "basis": "unknown", + "sources": { + "s215dc07e3642": "https://docs.github.com/en/actions/reference/security/secure-use", + "s92e8e4820392": "https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/manage-access", + "sbbcb15f795a8": "https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows", + "s4561d20e7042": "https://docs.github.com/en/actions/concepts/security/github_token", + "s80fe11d8c90d": "https://docs.github.com/en/actions/reference/security/oidc" + } + }, + "github-api": { + "name": "GitHub REST API", + "basis": "2026-03-10", + "sources": { + "s8217448953a4": "https://docs.github.com/en/rest/actions/self-hosted-runners", + "sd36db9ea1031": "https://docs.github.com/en/rest/actions/self-hosted-runner-groups", + "sbeee4a47110c": "https://docs.github.com/en/rest/actions/workflow-jobs" + } + }, + "gitlab": { + "name": "GitLab rolling documentation", + "basis": "unknown", + "sources": { + "seb30e3937183": "https://docs.gitlab.com/runner/security/", + "sde661ff4762e": "https://docs.gitlab.com/runner/executors/docker_autoscaler/", + "s265254bc5592": "https://docs.gitlab.com/runner/executors/docker/", + "s47b236d63dc4": "https://docs.gitlab.com/runner/configuration/advanced-configuration/", + "s1579c499c75b": "https://docs.gitlab.com/runner/executors/kubernetes/", + "sd2cbff5d5d6e": "https://docs.gitlab.com/ci/runners/new_creation_workflow/", + "se430cbabc011": "https://docs.gitlab.com/api/users/#create-a-runner-linked-to-a-user", + "saef0483b7be7": "https://docs.gitlab.com/ci/jobs/ci_job_token/", + "s699c1c198119": "https://docs.gitlab.com/api/project_job_token_scopes/", + "sbc009ff2866a": "https://docs.gitlab.com/runner/monitoring/" + } + }, + "docker": { + "name": "Docker Engine documentation", + "basis": "unknown", + "sources": { + "s18f59818e451": "https://docs.docker.com/engine/security/" + } + }, + "python": { + "name": "Python tomllib minimum", + "basis": "3.11", + "sources": { + "s37be9e98ea29": "https://docs.python.org/3.11/library/tomllib.html" + } + }, + "aws": { + "name": "AWS EC2 documentation", + "basis": "unknown", + "sources": { + "s82aca706445c": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html" + } + } + }, + "claims": { + "job-transport": {"text": "Both runners poll over HTTPS without an inbound job-polling port; verify GitLab TLS and restrict separate autoscaler management or webhook listeners.", "components": ["github", "gitlab"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "gitlab:sde661ff4762e"], "status": "REASONED"}, + "worker-account": {"text": "Use a dedicated least-privileged account, keep provisioning credentials off workers and confine rootful Docker access to disposable VMs.", "components": ["github", "gitlab", "docker"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "docker:s18f59818e451"], "status": "REASONED"}, + "worker-egress": {"text": "Enforce job egress outside job control, isolate production and other workers, and deny unused metadata over both families; IMDSv2 does not stop arbitrary authorized job code.", "components": ["github", "gitlab", "aws"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "aws:s82aca706445c"], "status": "REASONED"}, + "github-eligibility": {"text": "Keep self-hosted runners off public repositories; runner groups admit only private repositories by default and the public override should stay off.", "components": ["github"], "sources": ["github:s92e8e4820392", "github:s215dc07e3642"], "status": "REASONED"}, + "fork-token": {"text": "Ordinary fork pull_request jobs normally receive a read-only GITHUB_TOKEN and no other secrets, but can still read host credentials and reachable services.", "components": ["github"], "sources": ["github:s215dc07e3642", "github:sbbcb15f795a8"], "status": "REASONED"}, + "privileged-triggers": {"text": "pull_request_target and workflow_run can access secrets and write tokens; never execute untrusted PR code, artifacts or caches in these jobs.", "components": ["github"], "sources": ["github:sbbcb15f795a8", "github:s215dc07e3642"], "status": "REASONED"}, + "private-forks": {"text": "Review private-repository fork settings, which can grant write tokens and secrets; separate trusted privileged workers from untrusted CI.", "components": ["github"], "sources": ["github:s215dc07e3642"], "status": "REASONED"}, + "registration-token": {"text": "Registration tokens last one hour; generate on demand, never bake into images, and do not confuse expiry with runner deregistration.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, + "registration-manager": {"text": "Keep the App/PAT generating registration or JIT credentials off workers; organization endpoints need Self-hosted runners: write, repository endpoints Administration: write.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, + "jit": {"text": "Protect encoded_jit_config and the whole response; JIT runners execute at most one job.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, + "runner-credential": {"text": "Protect the installed runner authentication credential, directory and backups.", "components": ["github"], "sources": ["github:s215dc07e3642"], "status": "REASONED"}, + "github-job-token": {"text": "GITHUB_TOKEN is repository-scoped and per-job; set read-only defaults and grant only trusted jobs additional permissions.", "components": ["github"], "sources": ["github:s4561d20e7042"], "status": "REASONED"}, + "oidc": {"text": "Grant id-token: write only where needed; the cloud trust must restrict issuer, audience and repository/branch or protected-environment subject.", "components": ["github"], "sources": ["github:s80fe11d8c90d"], "status": "REASONED"}, + "github-lifecycle": {"text": "Prefer ephemeral/JIT workers and destroy their machines and storage; deregistration does not destroy disks, and labels route rather than authorize jobs.", "components": ["github", "github-api"], "sources": ["github:s215dc07e3642", "github:s92e8e4820392", "github-api:s8217448953a4"], "status": "REASONED"}, + "shell-executor": {"text": "The GitLab shell executor runs as the runner user without isolation; avoid it for untrusted jobs on shared hosts.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183"], "status": "REASONED"}, + "docker-executor": {"text": "Use unprivileged Docker; privileged=true or services_privileged and host Docker socket/TCP access can expose the host despite privileged=false.", "components": ["gitlab", "docker"], "sources": ["gitlab:s265254bc5592", "gitlab:seb30e3937183", "docker:s18f59818e451"], "status": "REASONED"}, + "executor-host-access": {"text": "Deny sensitive host mounts, host PID/network namespaces, unnecessary devices and capabilities; build without the host daemon.", "components": ["gitlab"], "sources": ["gitlab:s265254bc5592", "gitlab:s47b236d63dc4"], "status": "REASONED"}, + "kubernetes-executor": {"text": "Enforce nonprivileged containers, no privilege escalation, scoped ServiceAccounts and no unnecessary token mounting; prevent job overrides.", "components": ["gitlab"], "sources": ["gitlab:s1579c499c75b"], "status": "REASONED"}, + "gitlab-disposal": {"text": "Containers share the kernel; use disposable separate VMs/nodes as needed and one-job autoscaling workers. Instance and Docker Autoscaler GA is recorded as Runner 17.1.", "components": ["gitlab"], "sources": ["gitlab:sde661ff4762e", "gitlab:seb30e3937183"], "status": "REASONED"}, + "gitlab-registration": {"text": "Legacy registration tokens were disabled by default in GitLab 17.0, with removal scheduled for 20.0; use runner authentication tokens.", "components": ["gitlab"], "sources": ["gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, + "gitlab-runner-token": {"text": "Protect and rotate the runner authentication token in config.toml and backups; a cloned runner can steal assigned jobs.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, + "gitlab-creation": {"text": "POST /user/runners needs create_runner scope; keep that access token off workers and set scope/protection at creation.", "components": ["gitlab"], "sources": ["gitlab:se430cbabc011", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, + "gitlab-job-token": {"text": "CI_JOB_TOKEN carries supported triggering-user access during the job and appears as CI_REGISTRY_PASSWORD and inside CI_REPOSITORY_URL.", "components": ["gitlab"], "sources": ["gitlab:saef0483b7be7"], "status": "REASONED"}, + "gitlab-allowlist": {"text": "Target-project inbound job-token allowlists limit who may access that target, not job egress; also restrict resource feature visibility.", "components": ["gitlab"], "sources": ["gitlab:saef0483b7be7", "gitlab:s699c1c198119"], "status": "REASONED"}, + "gitlab-forks": {"text": "Instance runners on public installations accept strangers; project scope does not establish code trust. Review parent-launched fork pipelines or disable ci_allow_fork_pipelines_to_run_in_parent_project.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183"], "status": "REASONED"}, + "gitlab-protected": {"text": "Developer access can compromise persistent runners; release runners need ref_protected and a pool separate from fork-merge-request testing.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, + "gitlab-metrics": {"text": "Optional listen_address metrics and profiling have no built-in authorization; 9252 is allocated, not proof of the configured port.", "components": ["gitlab"], "sources": ["gitlab:sbc009ff2866a"], "status": "REASONED"}, + "gitlab-session": {"text": "Optional session_server uses TLS and must admit required GitLab traffic at its configured address; 8093 is an example, not an established default.", "components": ["gitlab"], "sources": ["gitlab:s47b236d63dc4"], "status": "REASONED"}, + "verify-eligibility": {"text": "Fully paginate groups, selected repositories and public repositories' own runners; visibility=all alone is not public eligibility, and inherited access needs review.", "components": ["github-api"], "sources": ["github-api:sd36db9ea1031", "github-api:s8217448953a4"], "status": "REASONED", "verify": [1]}, + "verify-permissions": {"text": "Inspect organization and repository workflow-token defaults and PR approval settings; explicit workflow permissions can raise grants and id-token is separate.", "components": ["github"], "sources": ["github:s4561d20e7042", "github:s80fe11d8c90d"], "status": "REASONED", "verify": [2]}, + "verify-assignment": {"text": "A controlled fork workflow should run when exposed and lose assignment when restricted, while an allowed private job still runs; skipped/pending/offline/timeout is inconclusive.", "components": ["github-api", "github"], "sources": ["github-api:sbeee4a47110c", "github:s92e8e4820392"], "status": "REASONED", "verify": [3]}, + "verify-config": {"text": "Python 3.14.4 synthetic fixtures reported exposed/fixed Docker, Kubernetes and autoscaler values; wrong types passed through, misplaced keys printed null, and empty/error cases differed in output and exit status.", "components": ["python", "gitlab"], "sources": ["python:s37be9e98ea29", "gitlab:s47b236d63dc4", "gitlab:s1579c499c75b"], "status": "DEMONSTRATED", "verify": [4], "evidence": "the extracted Python program ran without sudo on 20 synthetic config.toml cases using Python 3.14.4. Docker fixtures printed true/true, a socket mount and a TCP host versus false/false, empty volumes and a null host; both exited 0. Autoscaler limits printed null/null, 1/2 and 2/1 versus 1/1, all exit 0. Kubernetes fixtures printed true versus false for the three booleans and the supplied service_account. Wrong-type strings, booleans and floats retained their JSON types; misplaced privileged printed docker.privileged null. Empty and runner-less files printed nothing and exited 0. Missing files, malformed TOML and wrong section types exited 1; the second-runner structure error followed one printed runner. Two valid runners printed indices 0 and 1. The runner token key was omitted; a dummy token copied into service_account was printed. These observations cover file inspection only."}, + "verify-daemon": {"text": "Resolve docker.host through DOCKER_HOST or the default socket when absent; flag actual job access to a rootful daemon, not tcp:// alone.", "components": ["gitlab", "docker"], "sources": ["gitlab:s265254bc5592", "docker:s18f59818e451"], "status": "REASONED"}, + "verify-autoscaler": {"text": "For Instance/Docker Autoscaler require capacity_per_instance=1 and max_use_count=1 explicitly as integers; absent or other values are findings.", "components": ["gitlab"], "sources": ["gitlab:sde661ff4762e"], "status": "REASONED"}, + "verify-gitlab-api": {"text": "Read inbound allowlists and require release-runner ref_protected against exposed controls; gitlab-runner verify proves authentication only.", "components": ["gitlab"], "sources": ["gitlab:s699c1c198119", "gitlab:seb30e3937183"], "status": "REASONED", "verify": [5]}, + "verify-lifecycle": {"text": "Check complete deregistration listings and infrastructure disposal; persistent-worker canary must survive in the control, but absence alone does not prove destruction or clean shared caches.", "components": ["github-api", "gitlab", "github"], "sources": ["github-api:s8217448953a4", "gitlab:sde661ff4762e", "github:s215dc07e3642"], "status": "REASONED"}, + "verify-listeners": {"text": "Inventory namespaces and publications, require the permitted-client response, then test disallowed access; any HTTP response proves reachability, failure needs corroborating firewall denial.", "components": ["gitlab"], "sources": ["gitlab:sbc009ff2866a", "gitlab:s47b236d63dc4"], "status": "REASONED", "verify": [6]} + } +} +```` + diff --git a/self-signed.md b/self-signed.md index d2b5dac7f..4b9bda0d6 100644 --- a/self-signed.md +++ b/self-signed.md @@ -1,5 +1,203 @@ ---- -version_basis: { +# Self-signed certificates + +Written against OpenSSL documentation (rolling) (no recorded version), Requests TLS documentation v2.32.3, Git ignore documentation v2.43.0, curl command documentation curl-8_12_1, Node.js extra CA certificates v22.19.0, RFC 9525 service identity RFC 9525, Debian ca-certificates trixie, and Ubuntu ca-certificates noble, plus 3 more recorded bases; 1 further source carries no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 24 claims: 1 demonstrated by a check that ran and 23 reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + +Use a self-signed certificate when the service has no public DNS name, when an ACME CA cannot reach it, and when [cloudflare.md](cloudflare.md) is not an option: internal tools, lab and development environments, and machine-to-machine links on private networks. For anything a browser user or external party reaches, prefer [free-certificates.md](free-certificates.md); self-signed certificates trigger browser warnings and every client must be configured to trust them. + +Self-signed TLS still matters. It encrypts credentials and data in transit; without it, authentication tokens cross the network in cleartext. + +## 1. Generate a certificate with OpenSSL + +RSA, single command (OpenSSL 3.0 or later): + +```bash +openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -noenc \ + -keyout server.key -out server.crt \ + -subj "/CN=app.internal" \ + -addext "basicConstraints=critical,CA:FALSE" \ + -addext "subjectAltName=DNS:app.internal,DNS:localhost,IP:127.0.0.1,IP:203.0.113.10" +``` + +ECDSA (smaller and faster; generate the key first, then the certificate): + +```bash +openssl ecparam -name prime256v1 -genkey -noout -out server.key +openssl req -x509 -key server.key -sha256 -days 365 -out server.crt \ + -subj "/CN=app.internal" \ + -addext "basicConstraints=critical,CA:FALSE" \ + -addext "subjectAltName=DNS:app.internal,IP:203.0.113.10" +``` + +Rules that make the certificate actually work: + +- The `subjectAltName` list must contain every DNS name and IP address clients will use to reach the service (this guide's requirement, based on the RFC 9525 matching rules that follow). RFC 9525 requires DNS identity matching by comparing labels ('Each label MUST match', except under its wildcard rule) and says an IP-ID reference identifier 'MUST exactly match the value of an iPAddress entry in subjectAltName'. It says the subject Common Name 'MUST NOT be used to identify a service'. If no presented identifier matches, RFC 9525 says an automated client 'SHOULD terminate' and permits a configured exception that 'MUST NOT' disable this security control by default, while a human-facing client 'SHOULD' warn and terminate but may offer advanced users an override. Its ad hoc pinning provision says such pinning 'SHOULD NOT' restrict future connections to only the pinned certificate. Some verifiers, including OpenSSL's default, still fall back to the CN, so confirm in Verify below that the SAN exists rather than trusting a CN. Replace the example `203.0.113.10` (a documentation address from RFC 5737) with your service's real IP, or drop the `IP:` entries if clients reach it only by name. +- `basicConstraints=critical,CA:FALSE` marks the certificate a leaf, not a CA. It matters because section 4 installs the certificate into client trust stores: OpenSSL's stock configuration marks a bare `req -x509` certificate `CA:TRUE`, which in a trust store is a signer for any name, using the unencrypted key that `-noenc` leaves on the server. This override relies on OpenSSL 3.0 or later, where a command-line extension replaces the configuration default; the end-of-life 1.1.1 would instead emit a duplicate, invalid extension. +- `-noenc` (spelled `-nodes` before OpenSSL 3.0, still accepted but deprecated) leaves the key unencrypted so services can start unattended; protect it with file permissions instead. +- Track the `-days` expiry. Nothing renews a self-signed certificate for you; put the date in your calendar or monitoring. + +## 2. Protect the private key + +This guide advises protecting `server.key` with mode `600` and ownership by the account and group your service runs as; `app:app` below is this guide's example, to replace with that account and group. GNU coreutils v9.7 documents `chmod` changing file access permissions and taking new mode bits, and `chown owner:group` changing both owner and group (see Sources); it does not recommend a service account or select one for this key. This guide's reading of `600` is owner read/write and no group or other permissions, based on GNU coreutils v9.7's Numeric Modes definition in `doc/perm.texi` (see Sources). + +```bash +chmod 600 server.key +chown app:app server.key # whichever user your service runs as +``` + +Never commit a private key to version control. Add `*.key` and `*.pem` to `.gitignore` before generating anything inside a repository, and treat any key that has ever been committed or pasted into a chat as compromised: regenerate it. + +## 3. mkcert for local development + +[mkcert](https://github.com/FiloSottile/mkcert) creates a local CA, installs it into your OS and browser trust stores, and issues certificates that your own machine trusts with no warnings: + +```bash +mkcert -install +mkcert app.test localhost 127.0.0.1 ::1 +``` + +This is for development machines only. The generated CA can sign for any name, so its key must never leave the developer's machine, and mkcert certificates must never serve real users. + +## 4. Make clients trust the certificate; never disable verification + +Distribute the certificate (or your internal CA certificate) to clients instead of turning verification off: + +```bash +# Debian trixie/Ubuntu noble system trust store; other releases: this guide's assessment +sudo cp server.crt /usr/local/share/ca-certificates/app-internal.crt +sudo update-ca-certificates + +# RHEL 9 system trust store; Fedora and other RHEL releases: this guide's assessment +sudo cp server.crt /etc/pki/ca-trust/source/anchors/app-internal.crt +sudo update-ca-trust extract + +# Per-tool +curl -q --cacert server.crt https://app.internal/ +export REQUESTS_CA_BUNDLE=/path/to/server.crt # Python requests +export NODE_EXTRA_CA_CERTS=/path/to/server.crt # Node.js +``` + +Do not ship `curl -k`, `verify=False`, `rejectUnauthorized: false`, or `NODE_TLS_REJECT_UNAUTHORIZED=0` in committed code. For curl and Requests, `-k` and `verify=False` disable certificate verification (see their existing Sources entries). Node.js v24.22.0 documents `NODE_TLS_REJECT_UNAUTHORIZED=0` as an environment variable disabling certificate validation for TLS connections in the Node.js process and strongly discourages its use. In `tls.connect`, `rejectUnauthorized: false` disables verification of the server certificate against supplied CAs for that client connection. In `tls.createServer`, it disables rejection of connections unauthorized by supplied CAs; the option only has an effect when `requestCert` is `true`. These Node.js passages establish the documented settings and their scope, not their use in a particular deployment. This guide's assessment is that these bypasses accept attacker-controlled certificates as readily as your own and reliably survive into production. + +## 5. Verify + +DEMONSTRATED: following block; local certificate-output inspection only. On 2026-10-05 in private /dev/shm scratch, OpenSSL 3.5.5 ran the printed x509 command on three disposable one-day RSA certificates. Missing-SAN and CA:TRUE fixtures each exited 0 but lacked the required SAN or leaf constraint; the fixed fixture also exited 0 and printed DNS:app.internal, IP Address:127.0.0.1 and critical CA:FALSE. Subject and validity dates printed for all three. + +```bash +openssl x509 -in server.crt -noout -subject -dates -ext subjectAltName,basicConstraints +``` + +REASONED: following block; live TLS identity, failure handling and client-trust checks follow the cited OpenSSL and mkcert documentation. No app.internal service or client trust-store installation was supplied for these checks. The local x509 run above does not demonstrate a handshake or installed trust; expected outcomes and limitations are stated below. + +```bash +openssl s_client -connect app.internal:443 -servername app.internal -verify_hostname app.internal -verify_return_error -CAfile server.crt ` to check an IP identity, since `-verify_hostname` falls back to the Common Name whenever the certificate carries no DNS-name SAN entry (even if it has an IP SAN), so a CN-only or IP-only certificate can still pass a hostname check. The `s_client` check supplies trust explicitly with `-CAfile`, so it proves the certificate and name but not that the section-4 trust-store install took effect; confirm that separately from each client with its normal configuration (for the system store, a plain `curl https://app.internal/` with no `--cacert` should fail before `update-ca-certificates` and succeed after). + +## Limits to plan around + +Self-signed certificates have no revocation and no third-party accountability, and trust distribution is manual. When the audience grows beyond a small internal group, move to [free-certificates.md](free-certificates.md) or put the service behind [cloudflare.md](cloudflare.md). Authentication is still required either way: see [authentication.md](authentication.md). + +## Sources (checked September 2026) + +- OpenSSL documentation (rolling documentation, checked September 2026): https://docs.openssl.org/ ; `openssl s_client` (`-servername`, `-verify_return_error`): https://docs.openssl.org/master/man1/openssl-s_client/ ; verification options (`-verify_hostname`): https://docs.openssl.org/master/man1/openssl-verification-options/ +- mkcert: https://github.com/FiloSottile/mkcert +- Requests v2.32.3 CA-bundle environment variables (checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L238-L239 +- Requests v2.32.3 disabled certificate verification (checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L246-L250 +- Git v2.43.0 ignore rules and already tracked files (checked October 2026): https://github.com/git/git/blob/v2.43.0/Documentation/gitignore.txt#L15-L18 +- curl curl-8_12_1 explicit CA trust (checked October 2026): https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/cacert.md#L21-L24 +- curl curl-8_12_1 normal certificate verification and `--insecure` (checked October 2026): https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/insecure.md#L21-L28 +- Node.js v22.19.0 extra trusted CA certificates (checked October 2026): https://github.com/nodejs/node/blob/v22.19.0/doc/api/cli.md#L3234-L3238 +- RFC 9525 subject Common Name (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-2 +- RFC 9525 reference identifiers and IP-ID matching (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.2 +- RFC 9525 DNS identity matching (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.3 +- RFC 9525 IP identity matching (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.4 +- RFC 9525 identity mismatch outcome (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.6 +- Debian trixie update-ca-certificates (checked October 2026): https://manpages.debian.org/trixie/ca-certificates/update-ca-certificates.8.en.html#DESCRIPTION +- Ubuntu noble update-ca-certificates (checked October 2026): https://manpages.ubuntu.com/manpages/noble/man8/update-ca-certificates.8.html#description +- RHEL 9 shared system certificates (checked October 2026): https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/securing_networks/using-shared-system-certificates_securing-networks#adding-new-certificates_using-shared-system-certificates +- GNU coreutils v9.7 `chmod` syntax (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11937-L11942 +- GNU coreutils v9.7 mode argument (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11967-L11968 +- GNU coreutils v9.7 `chown` owner and group (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11614-L11622 +- GNU coreutils v9.7 numeric mode definition (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/perm.texi#L492-L535 +- Node.js v24.22.0 `NODE_TLS_REJECT_UNAUTHORIZED` (checked October 2026): https://github.com/nodejs/node/blob/v24.22.0/doc/api/cli.md#L3940-L3944 +- Node.js v24.22.0 `tls.connect` client `rejectUnauthorized` (checked October 2026): https://github.com/nodejs/node/blob/v24.22.0/doc/api/tls.md#L1786-L1789 +- Node.js v24.22.0 `tls.createServer` `rejectUnauthorized` and `requestCert` (checked October 2026): https://github.com/nodejs/node/blob/v24.22.0/doc/api/tls.md#L2250-L2255 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/self-signed.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- openssl: OpenSSL documentation (rolling) (unknown) +- mkcert: mkcert (unknown) +- requests: Requests TLS documentation (v2.32.3) +- git: Git ignore documentation (v2.43.0) +- curl-source: curl command documentation (curl-8_12_1) +- node: Node.js extra CA certificates (v22.19.0) +- rfc9525: RFC 9525 service identity (RFC 9525) +- debian-ca: Debian ca-certificates (trixie) +- ubuntu-ca: Ubuntu ca-certificates (noble) +- rhel-ca: RHEL shared system certificates (RHEL 9) +- coreutils: GNU coreutils file permissions (v9.7) +- node-v24: Node.js API documentation (v24.22.0) + +Claims (full source mapping in the JSON block below): + +- trust-scope (REASONED; openssl, mkcert): Self-signed TLS encrypts transit but needs explicit client trust and triggers browser warnings; the guide prefers publicly trusted certificates for browser users and external parties. +- rsa-certificate (REASONED; openssl): OpenSSL 3.0 or later generates an RSA 4096-bit self-signed certificate with SHA-256, 365-day validity, an unencrypted key and the shown subject and extensions. +- ec-certificate (REASONED; openssl): Generate a prime256v1 EC key first, then a SHA-256 self-signed certificate with 365-day validity and the shown subject and extensions; the guide describes ECDSA as smaller and faster. +- san (REASONED; openssl, rfc9525): SAN must cover every client DNS name and IP (this guide's requirement, based on RFC 9525's matching rules); replace the documentation IP or omit IP entries for name-only access. RFC 9525 requires DNS identity matching label by label except under its wildcard rule; an IP-ID reference identifier MUST exactly match an iPAddress subjectAltName entry. It forbids the subject Common Name as a service identifier. On mismatch, an automated client SHOULD terminate; a configured exception MUST NOT disable this control by default. A human-facing client SHOULD warn and terminate but MAY offer advanced users an override. Ad hoc pinning SHOULD NOT restrict future connections to only the pinned certificate. +- leaf-constraint (REASONED; openssl): Set basicConstraints=critical,CA:FALSE because the stated stock req -x509 configuration otherwise marks a trusted certificate CA:TRUE, allowing its unencrypted server key to sign for any name. +- extension-history (REASONED; openssl): The command-line extension override relies on OpenSSL 3.0 or later; the guide says end-of-life 1.1.1 instead emits a duplicate invalid extension. +- unencrypted-key (REASONED; openssl): -noenc leaves the key unencrypted for unattended startup; -nodes is the pre-3.0 spelling, still accepted but deprecated. Protect the key with permissions. +- expiry (REASONED; openssl): Track the -days expiry with a calendar or monitoring; self-signed certificates do not renew themselves. +- key-permissions (REASONED; openssl, coreutils): This guide advises chmod 600 and service-user ownership for server.key; GNU coreutils v9.7 documents chmod's mode changes and chown's owner/group changes, not that advice. Mode 600's owner read/write and no group/other permissions are this guide's reading of GNU coreutils v9.7's Numeric Modes definition in doc/perm.texi. +- key-compromise (REASONED; openssl, git): Never commit private keys; ignore *.key and *.pem before generation in a repository and regenerate keys committed or pasted into chat. +- mkcert (REASONED; mkcert): mkcert -install creates and trusts a local CA; mkcert issues certificates for the listed development DNS names and IPs. +- mkcert-custody (REASONED; mkcert): Use mkcert only for development; its CA key can sign for any name, must not leave the developer's machine and must not serve real users. +- debian-trust (REASONED; mkcert, debian-ca, ubuntu-ca): Install app-internal.crt under /usr/local/share/ca-certificates and run update-ca-certificates on Debian trixie and Ubuntu noble; use on other Debian and Ubuntu releases is this guide's assessment, with platform-specific Sources entries for those releases absent. +- rhel-trust (REASONED; mkcert, rhel-ca): Install app-internal.crt under /etc/pki/ca-trust/source/anchors and run update-ca-trust extract on RHEL 9; Fedora and other RHEL releases are this guide's assessment, with platform-specific Sources entries for those releases absent. +- curl-trust (REASONED; openssl, curl-source): curl --cacert server.crt supplies explicit trust for app.internal. +- requests-trust (REASONED; openssl, requests): REQUESTS_CA_BUNDLE supplies the certificate path to Python requests. +- node-trust (REASONED; mkcert, node): NODE_EXTRA_CA_CERTS supplies additional client trust to Node.js. +- verification-bypasses (REASONED; openssl, requests, curl-source, node-v24): Do not commit curl -k, verify=False, rejectUnauthorized: false or NODE_TLS_REJECT_UNAUTHORIZED=0. Node.js v24.22.0 scopes the bypasses to process-environment TLS validation, client-connection server verification or server rejection with requestCert=true; accepting attacker certificates and persistence into production are this guide's assessment. +- verify-extensions (DEMONSTRATED; openssl): Inspect x509 output for SAN coverage and CA:FALSE, not its exit status: it can exit 0 with no SAN. +- verify-tls (REASONED; openssl): The app.internal:443 s_client check supplies SNI, hostname checking, verification errors and CAfile; a pass prints Verification: OK, Verified peername: app.internal and return code 0, and exits 0. +- verify-failures (REASONED; openssl): Without -verify_hostname the name is ignored; without -verify_return_error a verification failure can still complete the handshake and exit 0. Both flags make name or trust failures exit nonzero; wrong name reports code 62. +- verify-identity (REASONED; openssl): Use -verify_ip for IP identities; -verify_hostname can fall back to CN without a DNS SAN, even with an IP SAN, so CN-only or IP-only certificates can pass hostname checks. +- verify-store (REASONED; openssl, mkcert, curl-source): Explicit -CAfile proves certificate and name, not the system-store installation; test each normally configured client separately, expecting plain curl without --cacert to fail before trust installation and succeed after. +- trust-limits (REASONED; openssl, mkcert): Self-signed certificates have no revocation or third-party accountability and need manual trust distribution; move beyond them as the audience grows and retain authentication. + +````json +{ "schema": 1, "checked": "2026-10-19", "documentation_checked": "2026-09", @@ -130,177 +328,5 @@ version_basis: { "trust-limits": {"text": "Self-signed certificates have no revocation or third-party accountability and need manual trust distribution; move beyond them as the audience grows and retain authentication.", "components": ["openssl", "mkcert"], "sources": ["openssl:s211b1e6e4aa4", "mkcert:s41ae17dba4ce"], "status": "REASONED"} } } ---- -# Self-signed certificates - - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- openssl: OpenSSL documentation (rolling) (unknown) -- mkcert: mkcert (unknown) -- requests: Requests TLS documentation (v2.32.3) -- git: Git ignore documentation (v2.43.0) -- curl-source: curl command documentation (curl-8_12_1) -- node: Node.js extra CA certificates (v22.19.0) -- rfc9525: RFC 9525 service identity (RFC 9525) -- debian-ca: Debian ca-certificates (trixie) -- ubuntu-ca: Ubuntu ca-certificates (noble) -- rhel-ca: RHEL shared system certificates (RHEL 9) -- coreutils: GNU coreutils file permissions (v9.7) -- node-v24: Node.js API documentation (v24.22.0) - -Claims (full source mapping in the front matter): - -- trust-scope (REASONED; openssl, mkcert): Self-signed TLS encrypts transit but needs explicit client trust and triggers browser warnings; the guide prefers publicly trusted certificates for browser users and external parties. -- rsa-certificate (REASONED; openssl): OpenSSL 3.0 or later generates an RSA 4096-bit self-signed certificate with SHA-256, 365-day validity, an unencrypted key and the shown subject and extensions. -- ec-certificate (REASONED; openssl): Generate a prime256v1 EC key first, then a SHA-256 self-signed certificate with 365-day validity and the shown subject and extensions; the guide describes ECDSA as smaller and faster. -- san (REASONED; openssl, rfc9525): SAN must cover every client DNS name and IP (this guide's requirement, based on RFC 9525's matching rules); replace the documentation IP or omit IP entries for name-only access. RFC 9525 requires DNS identity matching label by label except under its wildcard rule; an IP-ID reference identifier MUST exactly match an iPAddress subjectAltName entry. It forbids the subject Common Name as a service identifier. On mismatch, an automated client SHOULD terminate; a configured exception MUST NOT disable this control by default. A human-facing client SHOULD warn and terminate but MAY offer advanced users an override. Ad hoc pinning SHOULD NOT restrict future connections to only the pinned certificate. -- leaf-constraint (REASONED; openssl): Set basicConstraints=critical,CA:FALSE because the stated stock req -x509 configuration otherwise marks a trusted certificate CA:TRUE, allowing its unencrypted server key to sign for any name. -- extension-history (REASONED; openssl): The command-line extension override relies on OpenSSL 3.0 or later; the guide says end-of-life 1.1.1 instead emits a duplicate invalid extension. -- unencrypted-key (REASONED; openssl): -noenc leaves the key unencrypted for unattended startup; -nodes is the pre-3.0 spelling, still accepted but deprecated. Protect the key with permissions. -- expiry (REASONED; openssl): Track the -days expiry with a calendar or monitoring; self-signed certificates do not renew themselves. -- key-permissions (REASONED; openssl, coreutils): This guide advises chmod 600 and service-user ownership for server.key; GNU coreutils v9.7 documents chmod's mode changes and chown's owner/group changes, not that advice. Mode 600's owner read/write and no group/other permissions are this guide's reading of GNU coreutils v9.7's Numeric Modes definition in doc/perm.texi. -- key-compromise (REASONED; openssl, git): Never commit private keys; ignore *.key and *.pem before generation in a repository and regenerate keys committed or pasted into chat. -- mkcert (REASONED; mkcert): mkcert -install creates and trusts a local CA; mkcert issues certificates for the listed development DNS names and IPs. -- mkcert-custody (REASONED; mkcert): Use mkcert only for development; its CA key can sign for any name, must not leave the developer's machine and must not serve real users. -- debian-trust (REASONED; mkcert, debian-ca, ubuntu-ca): Install app-internal.crt under /usr/local/share/ca-certificates and run update-ca-certificates on Debian trixie and Ubuntu noble; use on other Debian and Ubuntu releases is this guide's assessment, with platform-specific Sources entries for those releases absent. -- rhel-trust (REASONED; mkcert, rhel-ca): Install app-internal.crt under /etc/pki/ca-trust/source/anchors and run update-ca-trust extract on RHEL 9; Fedora and other RHEL releases are this guide's assessment, with platform-specific Sources entries for those releases absent. -- curl-trust (REASONED; openssl, curl-source): curl --cacert server.crt supplies explicit trust for app.internal. -- requests-trust (REASONED; openssl, requests): REQUESTS_CA_BUNDLE supplies the certificate path to Python requests. -- node-trust (REASONED; mkcert, node): NODE_EXTRA_CA_CERTS supplies additional client trust to Node.js. -- verification-bypasses (REASONED; openssl, requests, curl-source, node-v24): Do not commit curl -k, verify=False, rejectUnauthorized: false or NODE_TLS_REJECT_UNAUTHORIZED=0. Node.js v24.22.0 scopes the bypasses to process-environment TLS validation, client-connection server verification or server rejection with requestCert=true; accepting attacker certificates and persistence into production are this guide's assessment. -- verify-extensions (DEMONSTRATED; openssl): Inspect x509 output for SAN coverage and CA:FALSE, not its exit status: it can exit 0 with no SAN. -- verify-tls (REASONED; openssl): The app.internal:443 s_client check supplies SNI, hostname checking, verification errors and CAfile; a pass prints Verification: OK, Verified peername: app.internal and return code 0, and exits 0. -- verify-failures (REASONED; openssl): Without -verify_hostname the name is ignored; without -verify_return_error a verification failure can still complete the handshake and exit 0. Both flags make name or trust failures exit nonzero; wrong name reports code 62. -- verify-identity (REASONED; openssl): Use -verify_ip for IP identities; -verify_hostname can fall back to CN without a DNS SAN, even with an IP SAN, so CN-only or IP-only certificates can pass hostname checks. -- verify-store (REASONED; openssl, mkcert, curl-source): Explicit -CAfile proves certificate and name, not the system-store installation; test each normally configured client separately, expecting plain curl without --cacert to fail before trust installation and succeed after. -- trust-limits (REASONED; openssl, mkcert): Self-signed certificates have no revocation or third-party accountability and need manual trust distribution; move beyond them as the audience grows and retain authentication. +```` - -Use a self-signed certificate when the service has no public DNS name, when an ACME CA cannot reach it, and when [cloudflare.md](cloudflare.md) is not an option: internal tools, lab and development environments, and machine-to-machine links on private networks. For anything a browser user or external party reaches, prefer [free-certificates.md](free-certificates.md); self-signed certificates trigger browser warnings and every client must be configured to trust them. - -Self-signed TLS still matters. It encrypts credentials and data in transit; without it, authentication tokens cross the network in cleartext. - -## 1. Generate a certificate with OpenSSL - -RSA, single command (OpenSSL 3.0 or later): - -```bash -openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -noenc \ - -keyout server.key -out server.crt \ - -subj "/CN=app.internal" \ - -addext "basicConstraints=critical,CA:FALSE" \ - -addext "subjectAltName=DNS:app.internal,DNS:localhost,IP:127.0.0.1,IP:203.0.113.10" -``` - -ECDSA (smaller and faster; generate the key first, then the certificate): - -```bash -openssl ecparam -name prime256v1 -genkey -noout -out server.key -openssl req -x509 -key server.key -sha256 -days 365 -out server.crt \ - -subj "/CN=app.internal" \ - -addext "basicConstraints=critical,CA:FALSE" \ - -addext "subjectAltName=DNS:app.internal,IP:203.0.113.10" -``` - -Rules that make the certificate actually work: - -- The `subjectAltName` list must contain every DNS name and IP address clients will use to reach the service (this guide's requirement, based on the RFC 9525 matching rules that follow). RFC 9525 requires DNS identity matching by comparing labels ('Each label MUST match', except under its wildcard rule) and says an IP-ID reference identifier 'MUST exactly match the value of an iPAddress entry in subjectAltName'. It says the subject Common Name 'MUST NOT be used to identify a service'. If no presented identifier matches, RFC 9525 says an automated client 'SHOULD terminate' and permits a configured exception that 'MUST NOT' disable this security control by default, while a human-facing client 'SHOULD' warn and terminate but may offer advanced users an override. Its ad hoc pinning provision says such pinning 'SHOULD NOT' restrict future connections to only the pinned certificate. Some verifiers, including OpenSSL's default, still fall back to the CN, so confirm in Verify below that the SAN exists rather than trusting a CN. Replace the example `203.0.113.10` (a documentation address from RFC 5737) with your service's real IP, or drop the `IP:` entries if clients reach it only by name. -- `basicConstraints=critical,CA:FALSE` marks the certificate a leaf, not a CA. It matters because section 4 installs the certificate into client trust stores: OpenSSL's stock configuration marks a bare `req -x509` certificate `CA:TRUE`, which in a trust store is a signer for any name, using the unencrypted key that `-noenc` leaves on the server. This override relies on OpenSSL 3.0 or later, where a command-line extension replaces the configuration default; the end-of-life 1.1.1 would instead emit a duplicate, invalid extension. -- `-noenc` (spelled `-nodes` before OpenSSL 3.0, still accepted but deprecated) leaves the key unencrypted so services can start unattended; protect it with file permissions instead. -- Track the `-days` expiry. Nothing renews a self-signed certificate for you; put the date in your calendar or monitoring. - -## 2. Protect the private key - -This guide advises protecting `server.key` with mode `600` and ownership by the account and group your service runs as; `app:app` below is this guide's example, to replace with that account and group. GNU coreutils v9.7 documents `chmod` changing file access permissions and taking new mode bits, and `chown owner:group` changing both owner and group (see Sources); it does not recommend a service account or select one for this key. This guide's reading of `600` is owner read/write and no group or other permissions, based on GNU coreutils v9.7's Numeric Modes definition in `doc/perm.texi` (see Sources). - -```bash -chmod 600 server.key -chown app:app server.key # whichever user your service runs as -``` - -Never commit a private key to version control. Add `*.key` and `*.pem` to `.gitignore` before generating anything inside a repository, and treat any key that has ever been committed or pasted into a chat as compromised: regenerate it. - -## 3. mkcert for local development - -[mkcert](https://github.com/FiloSottile/mkcert) creates a local CA, installs it into your OS and browser trust stores, and issues certificates that your own machine trusts with no warnings: - -```bash -mkcert -install -mkcert app.test localhost 127.0.0.1 ::1 -``` - -This is for development machines only. The generated CA can sign for any name, so its key must never leave the developer's machine, and mkcert certificates must never serve real users. - -## 4. Make clients trust the certificate; never disable verification - -Distribute the certificate (or your internal CA certificate) to clients instead of turning verification off: - -```bash -# Debian trixie/Ubuntu noble system trust store; other releases: this guide's assessment -sudo cp server.crt /usr/local/share/ca-certificates/app-internal.crt -sudo update-ca-certificates - -# RHEL 9 system trust store; Fedora and other RHEL releases: this guide's assessment -sudo cp server.crt /etc/pki/ca-trust/source/anchors/app-internal.crt -sudo update-ca-trust extract - -# Per-tool -curl -q --cacert server.crt https://app.internal/ -export REQUESTS_CA_BUNDLE=/path/to/server.crt # Python requests -export NODE_EXTRA_CA_CERTS=/path/to/server.crt # Node.js -``` - -Do not ship `curl -k`, `verify=False`, `rejectUnauthorized: false`, or `NODE_TLS_REJECT_UNAUTHORIZED=0` in committed code. For curl and Requests, `-k` and `verify=False` disable certificate verification (see their existing Sources entries). Node.js v24.22.0 documents `NODE_TLS_REJECT_UNAUTHORIZED=0` as an environment variable disabling certificate validation for TLS connections in the Node.js process and strongly discourages its use. In `tls.connect`, `rejectUnauthorized: false` disables verification of the server certificate against supplied CAs for that client connection. In `tls.createServer`, it disables rejection of connections unauthorized by supplied CAs; the option only has an effect when `requestCert` is `true`. These Node.js passages establish the documented settings and their scope, not their use in a particular deployment. This guide's assessment is that these bypasses accept attacker-controlled certificates as readily as your own and reliably survive into production. - -## 5. Verify - -DEMONSTRATED: following block; local certificate-output inspection only. On 2026-10-05 in private /dev/shm scratch, OpenSSL 3.5.5 ran the printed x509 command on three disposable one-day RSA certificates. Missing-SAN and CA:TRUE fixtures each exited 0 but lacked the required SAN or leaf constraint; the fixed fixture also exited 0 and printed DNS:app.internal, IP Address:127.0.0.1 and critical CA:FALSE. Subject and validity dates printed for all three. - -```bash -openssl x509 -in server.crt -noout -subject -dates -ext subjectAltName,basicConstraints -``` - -REASONED: following block; live TLS identity, failure handling and client-trust checks follow the cited OpenSSL and mkcert documentation. No app.internal service or client trust-store installation was supplied for these checks. The local x509 run above does not demonstrate a handshake or installed trust; expected outcomes and limitations are stated below. - -```bash -openssl s_client -connect app.internal:443 -servername app.internal -verify_hostname app.internal -verify_return_error -CAfile server.crt ` to check an IP identity, since `-verify_hostname` falls back to the Common Name whenever the certificate carries no DNS-name SAN entry (even if it has an IP SAN), so a CN-only or IP-only certificate can still pass a hostname check. The `s_client` check supplies trust explicitly with `-CAfile`, so it proves the certificate and name but not that the section-4 trust-store install took effect; confirm that separately from each client with its normal configuration (for the system store, a plain `curl https://app.internal/` with no `--cacert` should fail before `update-ca-certificates` and succeed after). - -## Limits to plan around - -Self-signed certificates have no revocation and no third-party accountability, and trust distribution is manual. When the audience grows beyond a small internal group, move to [free-certificates.md](free-certificates.md) or put the service behind [cloudflare.md](cloudflare.md). Authentication is still required either way: see [authentication.md](authentication.md). - -## Sources (checked September 2026) - -- OpenSSL documentation (rolling documentation, checked September 2026): https://docs.openssl.org/ ; `openssl s_client` (`-servername`, `-verify_return_error`): https://docs.openssl.org/master/man1/openssl-s_client/ ; verification options (`-verify_hostname`): https://docs.openssl.org/master/man1/openssl-verification-options/ -- mkcert: https://github.com/FiloSottile/mkcert -- Requests v2.32.3 CA-bundle environment variables (checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L238-L239 -- Requests v2.32.3 disabled certificate verification (checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L246-L250 -- Git v2.43.0 ignore rules and already tracked files (checked October 2026): https://github.com/git/git/blob/v2.43.0/Documentation/gitignore.txt#L15-L18 -- curl curl-8_12_1 explicit CA trust (checked October 2026): https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/cacert.md#L21-L24 -- curl curl-8_12_1 normal certificate verification and `--insecure` (checked October 2026): https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/insecure.md#L21-L28 -- Node.js v22.19.0 extra trusted CA certificates (checked October 2026): https://github.com/nodejs/node/blob/v22.19.0/doc/api/cli.md#L3234-L3238 -- RFC 9525 subject Common Name (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-2 -- RFC 9525 reference identifiers and IP-ID matching (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.2 -- RFC 9525 DNS identity matching (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.3 -- RFC 9525 IP identity matching (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.4 -- RFC 9525 identity mismatch outcome (checked October 2026): https://www.rfc-editor.org/rfc/rfc9525.html#section-6.6 -- Debian trixie update-ca-certificates (checked October 2026): https://manpages.debian.org/trixie/ca-certificates/update-ca-certificates.8.en.html#DESCRIPTION -- Ubuntu noble update-ca-certificates (checked October 2026): https://manpages.ubuntu.com/manpages/noble/man8/update-ca-certificates.8.html#description -- RHEL 9 shared system certificates (checked October 2026): https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/securing_networks/using-shared-system-certificates_securing-networks#adding-new-certificates_using-shared-system-certificates -- GNU coreutils v9.7 `chmod` syntax (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11937-L11942 -- GNU coreutils v9.7 mode argument (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11967-L11968 -- GNU coreutils v9.7 `chown` owner and group (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11614-L11622 -- GNU coreutils v9.7 numeric mode definition (checked October 2026): https://github.com/coreutils/coreutils/blob/v9.7/doc/perm.texi#L492-L535 -- Node.js v24.22.0 `NODE_TLS_REJECT_UNAUTHORIZED` (checked October 2026): https://github.com/nodejs/node/blob/v24.22.0/doc/api/cli.md#L3940-L3944 -- Node.js v24.22.0 `tls.connect` client `rejectUnauthorized` (checked October 2026): https://github.com/nodejs/node/blob/v24.22.0/doc/api/tls.md#L1786-L1789 -- Node.js v24.22.0 `tls.createServer` `rejectUnauthorized` and `requestCert` (checked October 2026): https://github.com/nodejs/node/blob/v24.22.0/doc/api/tls.md#L2250-L2255 diff --git a/site/llms-full.txt b/site/llms-full.txt index 741b6d864..454fbfc7c 100644 --- a/site/llms-full.txt +++ b/site/llms-full.txt @@ -461,63 +461,9 @@ maintaining a private assessment indefinitely. # Free publicly trusted certificates (ACME) - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-17; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +Written against Let's Encrypt documentation (no recorded version), Certbot reconfigure minimum 2.3.0, Certbot user guide and source v2.3.0, Certbot Cloudflare DNS plugin v2.3.0, Caddy automatic HTTPS v2.11.4, certmagic ACME implementation v0.25.3, and Traefik ACME documentation v3.5, plus 3 more recorded bases; 3 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 31 claims, all reasoned from cited sources. -Components (recorded basis): - -- le: Let's Encrypt documentation (unknown) -- certbot: Certbot documentation (unknown) -- reconfigure: Certbot reconfigure minimum (2.3.0) -- zerossl: ZeroSSL (unknown) -- acme: acme.sh (unknown) -- caddy: Caddy automatic HTTPS (v2.11.4) -- certmagic: certmagic ACME implementation (v0.25.3) -- traefik: Traefik ACME documentation (v3.5) -- openssl-cli: OpenSSL certificate commands (3.0) -- curl-source: curl command documentation (curl-8_12_1) -- certbot-v230: Certbot user guide and source (v2.3.0) -- certbot-dns-cloudflare-v230: Certbot Cloudflare DNS plugin (v2.3.0) -- cloudflare-origin-ca: Cloudflare Origin CA documentation (checked October 2026) - -Claims (full source mapping in the front matter): - -- public-trust (REASONED; le, zerossl): ACME CAs such as Let's Encrypt and ZeroSSL issue free publicly trusted certificates; this guide advises preferring them for public DNS names, including private hosts using DNS-01. -- http-challenge (REASONED; le): HTTP-01 needs a public A/AAAA or CNAME pointing to the server and inbound internet access to port 80. -- alpn-challenge (REASONED; le, caddy, certmagic, traefik): TLS-ALPN-01 needs a public DNS record and inbound port 443; Caddy's ACME issuer enables the TLS-ALPN-01 solver when DNS-01 is not configured and TLS-ALPN-01 is not disabled, and can use an alternate local port if port 443 forwards to it; Traefik also uses TLS-ALPN-01. -- dns-challenge (REASONED; le, certbot, certbot-v230): DNS-01 needs control of the challenge TXT record, no public address record or inbound port, and supports wildcards; automated issuance and renewal need an automated way to update TXT records, and this guide advises a provider DNS plugin and scoped API credentials, while Certbot also supports manual DNS-01 and an authentication hook for automatic renewal. -- certbot-install (REASONED; certbot): Install Certbot and nginx/Apache plugins from the distribution, or the classic snap with /usr/bin/certbot pointing to /snap/bin/certbot. -- server-plugins (REASONED; certbot): certbot --nginx or --apache issues and installs certificates for the supplied domain names when the server is supported. -- standalone (REASONED; certbot): certonly --standalone binds port 80; stop its current owner and arrange webroot, a server plugin or port-freeing hooks for unattended renewal. -- webroot (REASONED; certbot): certonly --webroot uses /var/www/html to serve challenge files while the existing web server keeps running. -- dns-credentials (REASONED; certbot, certbot-dns-cloudflare-v230): The Cloudflare DNS plugin uses a credentials file and a Zone:DNS:Edit token limited to required zones; this guide requires never using the Global API Key and protecting the file with mode 600 in a 700 directory. -- certificate-paths (REASONED; certbot): Configure servers against stable /etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem paths for the chain and private key. -- renewal-schedule (REASONED; certbot, certbot-v230): The Certbot v2.3.0 user guide says most installations have a scheduled task running certbot renew and says that, on Linux and BSD, you can check cron or systemd timers if unsure; this guide requires confirming the task and testing renewal plus reload with --dry-run --run-deploy-hooks, including package and snap installations. -- deploy-hook (REASONED; certbot, certbot-v230): This guide requires persisting a reload hook during successful initial issuance or successful renewal, or installing an executable in renewal-hooks/deploy; Certbot v2.3.0 saves the renewal-relevant options set on the command line to a lineage after successful renewal, and a renew --deploy-hook invocation with no renewal due does not save that hook. -- reconfigure (REASONED; reconfigure): certbot reconfigure can add the reload hook later on Certbot 2.3.0 and later. -- expiry-monitoring (REASONED; le, certbot): Monitor the served certificate's expiry and renewal failures instead of relying on CA reminder emails; include a dry run in the deployment checklist. This guide assesses that any renewal step involving a person will fail at those scheduled shorter certificate lifetimes and that unattended automation is the only viable path. -- default-lifetime (REASONED; le): At the guide's September 2026 documentation check, Let's Encrypt's default certificate lifetime is 90 days. -- short-lifetime (REASONED; le): Optional 6-day short-lived certificates are available to every subscriber as of the guide's September 2026 check. -- lifetime-schedule (REASONED; le): The classic profile moves to 64 days on 2027-02-10 and 45 days on 2028-02-16; industry rules cap publicly trusted certificates at 47 days from 2029-03-15. -- revocation (REASONED; le): Let's Encrypt ended OCSP on 2025-08-06 and publishes revocation through CRLs only; do not add OCSP stapling directives for its certificates. -- staging (REASONED; le, certbot): Let's Encrypt limits per-domain issuance; test with certbot --staging or --test-cert before real issuance. -- caa (REASONED; le): CAA restricts issuing CAs, using issue letsencrypt.org for Let's Encrypt; certificate-transparency monitoring detects mis-issuance afterwards and complements CAA. -- zerossl-eab (REASONED; zerossl): ZeroSSL offers free ACME certificates; some clients need dashboard EAB credentials. -- acme-default (REASONED; acme): acme.sh registers with ZeroSSL by default. -- acme-install (REASONED; acme): acme.sh is a shell ACME client supporting many DNS providers; the guide recommends installation from a repository release instead of piping a download into a shell. -- server-automation (REASONED; le, caddy, traefik): This guide advises Caddy or Traefik for built-in issuance and renewal without an external client; it assesses them as the lowest-effort correct option for new deployments. -- origin-certificates (REASONED; le, cloudflare-origin-ca): Cloudflare lists Origin CA certificates on its Free plan, refers to long-lived origin CA certificates and says the validity period is chosen at creation, says they only encrypt traffic between Cloudflare and the origin, and warns that visitors may see untrusted certificate errors when Cloudflare is paused or proxying is disabled; this guide requires using them only for origins that receive traffic only through Cloudflare-proxied records. -- verify-inventory (REASONED; certbot): certbot certificates lists issued certificates and their expiry dates. -- verify-https (REASONED; le, curl-source): Use the deployment's public hostname rather than example.com; curl HTTPS should succeed without -k. -- verify-certificate (REASONED; le, openssl-cli): The OpenSSL probe targets the hostname on port 443 with SNI, hostname verification and verification errors enabled, then displays issuer and dates. -- authentication (REASONED; le): A certificate alone is insufficient protection; continue with server controls and authentication. -- dns-automation (REASONED; certbot, certbot-v230): For automated DNS-01 issuance and renewal, this guide advises using a provider DNS plugin and least-privilege API credentials; Certbot also documents manual TXT entry and authentication hooks that automate the challenge. -- dns-manual (REASONED; certbot, certbot-v230): certbot certonly --manual --preferred-challenges dns permits hand-created TXT records; the Certbot v2.3.0 guide says certificates created with --manual do not support automatic renewal without a --manual-auth-hook that automates the challenge. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Publicly trusted certificates are free through ACME certificate authorities such as Let's Encrypt and ZeroSSL. Browsers and libraries accept them without any client-side configuration, which is why this guide assesses them as the correct choice for every service with a public DNS name. Use [self-signed.md](self-signed.md) only when no public domain exists; a host that cannot accept inbound connections can still get a publicly trusted certificate through the DNS-01 challenge (below), or serve behind [cloudflare.md](cloudflare.md). @@ -663,62 +609,259 @@ REASONED: preceding block; certificate inventory, HTTPS access and served-certif - Certbot v2.3.0 Cloudflare DNS credential permissions (checked October 2026): - Cloudflare Origin CA (checked October 2026): https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/ -====================================================================== -==> self-signed.md -====================================================================== - -# Self-signed certificates +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/free-certificates.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + **Version basis** -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- openssl: OpenSSL documentation (rolling) (unknown) -- mkcert: mkcert (unknown) -- requests: Requests TLS documentation (v2.32.3) -- git: Git ignore documentation (v2.43.0) +- le: Let's Encrypt documentation (unknown) +- zerossl: ZeroSSL (unknown) +- certbot: Certbot documentation (unknown) +- reconfigure: Certbot reconfigure minimum (2.3.0) +- acme: acme.sh (unknown) +- certbot-v230: Certbot user guide and source (v2.3.0) +- certbot-dns-cloudflare-v230: Certbot Cloudflare DNS plugin (v2.3.0) +- caddy: Caddy automatic HTTPS (v2.11.4) +- certmagic: certmagic ACME implementation (v0.25.3) +- traefik: Traefik ACME documentation (v3.5) +- cloudflare-origin-ca: Cloudflare Origin CA documentation (checked October 2026) +- openssl-cli: OpenSSL certificate commands (3.0) - curl-source: curl command documentation (curl-8_12_1) -- node: Node.js extra CA certificates (v22.19.0) -- rfc9525: RFC 9525 service identity (RFC 9525) -- debian-ca: Debian ca-certificates (trixie) -- ubuntu-ca: Ubuntu ca-certificates (noble) -- rhel-ca: RHEL shared system certificates (RHEL 9) -- coreutils: GNU coreutils file permissions (v9.7) -- node-v24: Node.js API documentation (v24.22.0) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- trust-scope (REASONED; openssl, mkcert): Self-signed TLS encrypts transit but needs explicit client trust and triggers browser warnings; the guide prefers publicly trusted certificates for browser users and external parties. -- rsa-certificate (REASONED; openssl): OpenSSL 3.0 or later generates an RSA 4096-bit self-signed certificate with SHA-256, 365-day validity, an unencrypted key and the shown subject and extensions. -- ec-certificate (REASONED; openssl): Generate a prime256v1 EC key first, then a SHA-256 self-signed certificate with 365-day validity and the shown subject and extensions; the guide describes ECDSA as smaller and faster. -- san (REASONED; openssl, rfc9525): SAN must cover every client DNS name and IP (this guide's requirement, based on RFC 9525's matching rules); replace the documentation IP or omit IP entries for name-only access. RFC 9525 requires DNS identity matching label by label except under its wildcard rule; an IP-ID reference identifier MUST exactly match an iPAddress subjectAltName entry. It forbids the subject Common Name as a service identifier. On mismatch, an automated client SHOULD terminate; a configured exception MUST NOT disable this control by default. A human-facing client SHOULD warn and terminate but MAY offer advanced users an override. Ad hoc pinning SHOULD NOT restrict future connections to only the pinned certificate. -- leaf-constraint (REASONED; openssl): Set basicConstraints=critical,CA:FALSE because the stated stock req -x509 configuration otherwise marks a trusted certificate CA:TRUE, allowing its unencrypted server key to sign for any name. -- extension-history (REASONED; openssl): The command-line extension override relies on OpenSSL 3.0 or later; the guide says end-of-life 1.1.1 instead emits a duplicate invalid extension. -- unencrypted-key (REASONED; openssl): -noenc leaves the key unencrypted for unattended startup; -nodes is the pre-3.0 spelling, still accepted but deprecated. Protect the key with permissions. -- expiry (REASONED; openssl): Track the -days expiry with a calendar or monitoring; self-signed certificates do not renew themselves. -- key-permissions (REASONED; openssl, coreutils): This guide advises chmod 600 and service-user ownership for server.key; GNU coreutils v9.7 documents chmod's mode changes and chown's owner/group changes, not that advice. Mode 600's owner read/write and no group/other permissions are this guide's reading of GNU coreutils v9.7's Numeric Modes definition in doc/perm.texi. -- key-compromise (REASONED; openssl, git): Never commit private keys; ignore *.key and *.pem before generation in a repository and regenerate keys committed or pasted into chat. -- mkcert (REASONED; mkcert): mkcert -install creates and trusts a local CA; mkcert issues certificates for the listed development DNS names and IPs. -- mkcert-custody (REASONED; mkcert): Use mkcert only for development; its CA key can sign for any name, must not leave the developer's machine and must not serve real users. -- debian-trust (REASONED; mkcert, debian-ca, ubuntu-ca): Install app-internal.crt under /usr/local/share/ca-certificates and run update-ca-certificates on Debian trixie and Ubuntu noble; use on other Debian and Ubuntu releases is this guide's assessment, with platform-specific Sources entries for those releases absent. -- rhel-trust (REASONED; mkcert, rhel-ca): Install app-internal.crt under /etc/pki/ca-trust/source/anchors and run update-ca-trust extract on RHEL 9; Fedora and other RHEL releases are this guide's assessment, with platform-specific Sources entries for those releases absent. -- curl-trust (REASONED; openssl, curl-source): curl --cacert server.crt supplies explicit trust for app.internal. -- requests-trust (REASONED; openssl, requests): REQUESTS_CA_BUNDLE supplies the certificate path to Python requests. -- node-trust (REASONED; mkcert, node): NODE_EXTRA_CA_CERTS supplies additional client trust to Node.js. -- verification-bypasses (REASONED; openssl, requests, curl-source, node-v24): Do not commit curl -k, verify=False, rejectUnauthorized: false or NODE_TLS_REJECT_UNAUTHORIZED=0. Node.js v24.22.0 scopes the bypasses to process-environment TLS validation, client-connection server verification or server rejection with requestCert=true; accepting attacker certificates and persistence into production are this guide's assessment. -- verify-extensions (DEMONSTRATED; openssl): Inspect x509 output for SAN coverage and CA:FALSE, not its exit status: it can exit 0 with no SAN. -- verify-tls (REASONED; openssl): The app.internal:443 s_client check supplies SNI, hostname checking, verification errors and CAfile; a pass prints Verification: OK, Verified peername: app.internal and return code 0, and exits 0. -- verify-failures (REASONED; openssl): Without -verify_hostname the name is ignored; without -verify_return_error a verification failure can still complete the handshake and exit 0. Both flags make name or trust failures exit nonzero; wrong name reports code 62. -- verify-identity (REASONED; openssl): Use -verify_ip for IP identities; -verify_hostname can fall back to CN without a DNS SAN, even with an IP SAN, so CN-only or IP-only certificates can pass hostname checks. -- verify-store (REASONED; openssl, mkcert, curl-source): Explicit -CAfile proves certificate and name, not the system-store installation; test each normally configured client separately, expecting plain curl without --cacert to fail before trust installation and succeed after. -- trust-limits (REASONED; openssl, mkcert): Self-signed certificates have no revocation or third-party accountability and need manual trust distribution; move beyond them as the audience grows and retain authentication. +- public-trust (REASONED; le, zerossl): ACME CAs such as Let's Encrypt and ZeroSSL issue free publicly trusted certificates; this guide advises preferring them for public DNS names, including private hosts using DNS-01. +- http-challenge (REASONED; le): HTTP-01 needs a public A/AAAA or CNAME pointing to the server and inbound internet access to port 80. +- alpn-challenge (REASONED; le, caddy, certmagic, traefik): TLS-ALPN-01 needs a public DNS record and inbound port 443; Caddy's ACME issuer enables the TLS-ALPN-01 solver when DNS-01 is not configured and TLS-ALPN-01 is not disabled, and can use an alternate local port if port 443 forwards to it; Traefik also uses TLS-ALPN-01. +- dns-challenge (REASONED; le, certbot, certbot-v230): DNS-01 needs control of the challenge TXT record, no public address record or inbound port, and supports wildcards; automated issuance and renewal need an automated way to update TXT records, and this guide advises a provider DNS plugin and scoped API credentials, while Certbot also supports manual DNS-01 and an authentication hook for automatic renewal. +- certbot-install (REASONED; certbot): Install Certbot and nginx/Apache plugins from the distribution, or the classic snap with /usr/bin/certbot pointing to /snap/bin/certbot. +- server-plugins (REASONED; certbot): certbot --nginx or --apache issues and installs certificates for the supplied domain names when the server is supported. +- standalone (REASONED; certbot): certonly --standalone binds port 80; stop its current owner and arrange webroot, a server plugin or port-freeing hooks for unattended renewal. +- webroot (REASONED; certbot): certonly --webroot uses /var/www/html to serve challenge files while the existing web server keeps running. +- dns-credentials (REASONED; certbot, certbot-dns-cloudflare-v230): The Cloudflare DNS plugin uses a credentials file and a Zone:DNS:Edit token limited to required zones; this guide requires never using the Global API Key and protecting the file with mode 600 in a 700 directory. +- certificate-paths (REASONED; certbot): Configure servers against stable /etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem paths for the chain and private key. +- renewal-schedule (REASONED; certbot, certbot-v230): The Certbot v2.3.0 user guide says most installations have a scheduled task running certbot renew and says that, on Linux and BSD, you can check cron or systemd timers if unsure; this guide requires confirming the task and testing renewal plus reload with --dry-run --run-deploy-hooks, including package and snap installations. +- deploy-hook (REASONED; certbot, certbot-v230): This guide requires persisting a reload hook during successful initial issuance or successful renewal, or installing an executable in renewal-hooks/deploy; Certbot v2.3.0 saves the renewal-relevant options set on the command line to a lineage after successful renewal, and a renew --deploy-hook invocation with no renewal due does not save that hook. +- reconfigure (REASONED; reconfigure): certbot reconfigure can add the reload hook later on Certbot 2.3.0 and later. +- expiry-monitoring (REASONED; le, certbot): Monitor the served certificate's expiry and renewal failures instead of relying on CA reminder emails; include a dry run in the deployment checklist. This guide assesses that any renewal step involving a person will fail at those scheduled shorter certificate lifetimes and that unattended automation is the only viable path. +- default-lifetime (REASONED; le): At the guide's September 2026 documentation check, Let's Encrypt's default certificate lifetime is 90 days. +- short-lifetime (REASONED; le): Optional 6-day short-lived certificates are available to every subscriber as of the guide's September 2026 check. +- lifetime-schedule (REASONED; le): The classic profile moves to 64 days on 2027-02-10 and 45 days on 2028-02-16; industry rules cap publicly trusted certificates at 47 days from 2029-03-15. +- revocation (REASONED; le): Let's Encrypt ended OCSP on 2025-08-06 and publishes revocation through CRLs only; do not add OCSP stapling directives for its certificates. +- staging (REASONED; le, certbot): Let's Encrypt limits per-domain issuance; test with certbot --staging or --test-cert before real issuance. +- caa (REASONED; le): CAA restricts issuing CAs, using issue letsencrypt.org for Let's Encrypt; certificate-transparency monitoring detects mis-issuance afterwards and complements CAA. +- zerossl-eab (REASONED; zerossl): ZeroSSL offers free ACME certificates; some clients need dashboard EAB credentials. +- acme-default (REASONED; acme): acme.sh registers with ZeroSSL by default. +- acme-install (REASONED; acme): acme.sh is a shell ACME client supporting many DNS providers; the guide recommends installation from a repository release instead of piping a download into a shell. +- server-automation (REASONED; le, caddy, traefik): This guide advises Caddy or Traefik for built-in issuance and renewal without an external client; it assesses them as the lowest-effort correct option for new deployments. +- origin-certificates (REASONED; le, cloudflare-origin-ca): Cloudflare lists Origin CA certificates on its Free plan, refers to long-lived origin CA certificates and says the validity period is chosen at creation, says they only encrypt traffic between Cloudflare and the origin, and warns that visitors may see untrusted certificate errors when Cloudflare is paused or proxying is disabled; this guide requires using them only for origins that receive traffic only through Cloudflare-proxied records. +- verify-inventory (REASONED; certbot): certbot certificates lists issued certificates and their expiry dates. +- verify-https (REASONED; le, curl-source): Use the deployment's public hostname rather than example.com; curl HTTPS should succeed without -k. +- verify-certificate (REASONED; le, openssl-cli): The OpenSSL probe targets the hostname on port 443 with SNI, hostname verification and verification errors enabled, then displays issuer and dates. +- authentication (REASONED; le): A certificate alone is insufficient protection; continue with server controls and authentication. +- dns-automation (REASONED; certbot, certbot-v230): For automated DNS-01 issuance and renewal, this guide advises using a provider DNS plugin and least-privilege API credentials; Certbot also documents manual TXT entry and authentication hooks that automate the challenge. +- dns-manual (REASONED; certbot, certbot-v230): certbot certonly --manual --preferred-challenges dns permits hand-created TXT records; the Certbot v2.3.0 guide says certificates created with --manual do not support automatic renewal without a --manual-auth-hook that automates the challenge. + +````json +{ + "schema": 1, + "checked": "2026-10-17", + "documentation_checked": "2026-09", + "body_sha256": "1f578874874b2d4b757f649adda8745f4d241949ddc0c71bfffb7315a7158257", + "components": { + "le": { + "name": "Let's Encrypt documentation", + "basis": "unknown", + "sources": { + "s9ff2d787c618": "https://letsencrypt.org/docs/", + "s01388c85c5d6": "https://letsencrypt.org/docs/cert-lifetimes/", + "s4406ef210fff": "https://letsencrypt.org/2025/12/02/from-90-to-45", + "s21efa5995c48": "https://letsencrypt.org/2025/08/06/ocsp-service-has-reached-end-of-life", + "saf908bcd2143": "https://letsencrypt.org/docs/caa/" + } + }, + "zerossl": { + "name": "ZeroSSL", + "basis": "unknown", + "sources": { + "sddca28a3381b": "https://zerossl.com/" + } + }, + "certbot": { + "name": "Certbot documentation", + "basis": "unknown", + "sources": { + "s44faaf4eb7d7": "https://certbot.eff.org/", + "s7b60a00cadf3": "https://eff-certbot.readthedocs.io/en/stable/using.html#manual", + "s875be163b4c1": "https://eff-certbot.readthedocs.io/en/stable/using.html#dns-plugins" + } + }, + "reconfigure": { + "name": "Certbot reconfigure minimum", + "basis": "2.3.0", + "sources": { + "s0dee7d6d65ba": "https://raw.githubusercontent.com/certbot/certbot/v2.3.0/certbot/docs/using.rst" + } + }, + "acme": { + "name": "acme.sh", + "basis": "unknown", + "sources": { + "sa635c50fc1f9": "https://github.com/acmesh-official/acme.sh" + } + }, + "certbot-v230": { + "name": "Certbot user guide and source", + "basis": "v2.3.0", + "sources": { + "s725109195132": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L808-L825", + "s2f25bc233392": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L632-L640", + "s11c7994242fc": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L704-L710", + "sbb0c60d635ee": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L684", + "s55ec19f37395": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L194-L220", + "s5bf4ff9fbe76": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L222-L265", + "s7a053488f705": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/cli_utils.py#L227-L231", + "sa18c858c4e49": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/renewal.py#L40-L53", + "s2e4bd79a7795": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/renewal.py#L509-L534", + "s1910987bfd8c": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L295-L312", + "s9c6118b72e0a": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L1120-L1128", + "s3e22e32a2862": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/storage.py#L1243-L1249" + } + }, + "certbot-dns-cloudflare-v230": { + "name": "Certbot Cloudflare DNS plugin", + "basis": "v2.3.0", + "sources": { + "s3f07ef3d544d": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py#L24-L39", + "sc3b33fa2c90f": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py#L61-L78" + } + }, + "caddy": { + "name": "Caddy automatic HTTPS", + "basis": "v2.11.4", + "sources": { + "s9683cb8d689f": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L62-L63", + "s409f9221aec2": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L280-L296", + "s80dbf0751153": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L245-L249", + "s5216789c9341": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/acmeissuer.go#L321-L328" + } + }, + "certmagic": { + "name": "certmagic ACME implementation", + "basis": "v0.25.3", + "sources": { + "s61e3cba719df": "https://github.com/caddyserver/caddy/blob/v2.11.4/go.mod#L12-L12", + "s8782e006523b": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeclient.go#L188-L226", + "s465c4ddcc92f": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeclient.go#L304-L313", + "sc1e2ddefe5ed": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L116-L120", + "sd423f53faa1a": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L222-L236", + "sf2fbc79f6542": "https://github.com/caddyserver/certmagic/blob/v0.25.3/certmagic.go#L473-L475", + "s1c3236df58b9": "https://github.com/caddyserver/certmagic/blob/v0.25.3/acmeissuer.go#L647-L654" + } + }, + "traefik": { + "name": "Traefik ACME documentation", + "basis": "v3.5", + "sources": { + "s418a6b1ad09b": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/tls/certificate-resolvers/acme/" + } + }, + "cloudflare-origin-ca": { + "name": "Cloudflare Origin CA documentation", + "basis": "checked October 2026", + "sources": { + "s1e15de5c9f36": "https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/" + } + }, + "openssl-cli": { + "name": "OpenSSL certificate commands", + "basis": "3.0", + "sources": { + "s00aaf106164f": "https://docs.openssl.org/3.0/man1/openssl-s_client/", + "s0856f4a58574": "https://docs.openssl.org/3.0/man1/openssl-x509/" + } + }, + "curl-source": { + "name": "curl command documentation", + "basis": "curl-8_12_1", + "sources": { + "s76f69702c9d5": "https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/head.md#L21-L22", + "s60820cd231e1": "https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/insecure.md#L21-L28" + } + } + }, + "claims": { + "public-trust": {"text": "ACME CAs such as Let's Encrypt and ZeroSSL issue free publicly trusted certificates; this guide advises preferring them for public DNS names, including private hosts using DNS-01.", "components": ["le", "zerossl"], "sources": ["le:s9ff2d787c618", "zerossl:sddca28a3381b"], "status": "REASONED"}, + "http-challenge": {"text": "HTTP-01 needs a public A/AAAA or CNAME pointing to the server and inbound internet access to port 80.", "components": ["le"], "sources": ["le:s9ff2d787c618"], "status": "REASONED"}, + "alpn-challenge": {"text": "TLS-ALPN-01 needs a public DNS record and inbound port 443; Caddy's ACME issuer enables the TLS-ALPN-01 solver when DNS-01 is not configured and TLS-ALPN-01 is not disabled, and can use an alternate local port if port 443 forwards to it; Traefik also uses TLS-ALPN-01.", "components": ["le", "caddy", "certmagic", "traefik"], "sources": ["le:s9ff2d787c618", "caddy:s409f9221aec2", "caddy:s80dbf0751153", "caddy:s5216789c9341", "certmagic:s61e3cba719df", "certmagic:s8782e006523b", "certmagic:s465c4ddcc92f", "certmagic:sc1e2ddefe5ed", "certmagic:sd423f53faa1a", "certmagic:sf2fbc79f6542", "certmagic:s1c3236df58b9", "traefik:s418a6b1ad09b"], "status": "REASONED"}, + "dns-challenge": {"text": "DNS-01 needs control of the challenge TXT record, no public address record or inbound port, and supports wildcards; automated issuance and renewal need an automated way to update TXT records, and this guide advises a provider DNS plugin and scoped API credentials, while Certbot also supports manual DNS-01 and an authentication hook for automatic renewal.", "components": ["le", "certbot", "certbot-v230"], "sources": ["le:s9ff2d787c618", "certbot:s7b60a00cadf3", "certbot:s875be163b4c1", "certbot-v230:s55ec19f37395", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"}, + "certbot-install": {"text": "Install Certbot and nginx/Apache plugins from the distribution, or the classic snap with /usr/bin/certbot pointing to /snap/bin/certbot.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "server-plugins": {"text": "certbot --nginx or --apache issues and installs certificates for the supplied domain names when the server is supported.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "standalone": {"text": "certonly --standalone binds port 80; stop its current owner and arrange webroot, a server plugin or port-freeing hooks for unattended renewal.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "webroot": {"text": "certonly --webroot uses /var/www/html to serve challenge files while the existing web server keeps running.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "dns-credentials": {"text": "The Cloudflare DNS plugin uses a credentials file and a Zone:DNS:Edit token limited to required zones; this guide requires never using the Global API Key and protecting the file with mode 600 in a 700 directory.", "components": ["certbot", "certbot-dns-cloudflare-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-dns-cloudflare-v230:s3f07ef3d544d", "certbot-dns-cloudflare-v230:sc3b33fa2c90f"], "status": "REASONED"}, + "certificate-paths": {"text": "Configure servers against stable /etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem paths for the chain and private key.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "renewal-schedule": {"text": "The Certbot v2.3.0 user guide says most installations have a scheduled task running certbot renew and says that, on Linux and BSD, you can check cron or systemd timers if unsure; this guide requires confirming the task and testing renewal plus reload with --dry-run --run-deploy-hooks, including package and snap installations.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-v230:s725109195132"], "status": "REASONED"}, + "deploy-hook": {"text": "This guide requires persisting a reload hook during successful initial issuance or successful renewal, or installing an executable in renewal-hooks/deploy; Certbot v2.3.0 saves the renewal-relevant options set on the command line to a lineage after successful renewal, and a renew --deploy-hook invocation with no renewal due does not save that hook.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s44faaf4eb7d7", "certbot-v230:s2f25bc233392", "certbot-v230:s11c7994242fc", "certbot-v230:sbb0c60d635ee", "certbot-v230:s7a053488f705", "certbot-v230:sa18c858c4e49", "certbot-v230:s2e4bd79a7795", "certbot-v230:s1910987bfd8c", "certbot-v230:s9c6118b72e0a", "certbot-v230:s3e22e32a2862"], "status": "REASONED"}, + "reconfigure": {"text": "certbot reconfigure can add the reload hook later on Certbot 2.3.0 and later.", "components": ["reconfigure"], "sources": ["reconfigure:s0dee7d6d65ba"], "status": "REASONED"}, + "expiry-monitoring": {"text": "Monitor the served certificate's expiry and renewal failures instead of relying on CA reminder emails; include a dry run in the deployment checklist. This guide assesses that any renewal step involving a person will fail at those scheduled shorter certificate lifetimes and that unattended automation is the only viable path.", "components": ["le", "certbot"], "sources": ["le:s4406ef210fff", "certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "default-lifetime": {"text": "At the guide's September 2026 documentation check, Let's Encrypt's default certificate lifetime is 90 days.", "components": ["le"], "sources": ["le:s01388c85c5d6"], "status": "REASONED"}, + "short-lifetime": {"text": "Optional 6-day short-lived certificates are available to every subscriber as of the guide's September 2026 check.", "components": ["le"], "sources": ["le:s01388c85c5d6"], "status": "REASONED"}, + "lifetime-schedule": {"text": "The classic profile moves to 64 days on 2027-02-10 and 45 days on 2028-02-16; industry rules cap publicly trusted certificates at 47 days from 2029-03-15.", "components": ["le"], "sources": ["le:s01388c85c5d6", "le:s4406ef210fff"], "status": "REASONED"}, + "revocation": {"text": "Let's Encrypt ended OCSP on 2025-08-06 and publishes revocation through CRLs only; do not add OCSP stapling directives for its certificates.", "components": ["le"], "sources": ["le:s21efa5995c48"], "status": "REASONED"}, + "staging": {"text": "Let's Encrypt limits per-domain issuance; test with certbot --staging or --test-cert before real issuance.", "components": ["le", "certbot"], "sources": ["le:s9ff2d787c618", "certbot:s44faaf4eb7d7"], "status": "REASONED"}, + "caa": {"text": "CAA restricts issuing CAs, using issue letsencrypt.org for Let's Encrypt; certificate-transparency monitoring detects mis-issuance afterwards and complements CAA.", "components": ["le"], "sources": ["le:saf908bcd2143", "le:s9ff2d787c618"], "status": "REASONED"}, + "zerossl-eab": {"text": "ZeroSSL offers free ACME certificates; some clients need dashboard EAB credentials.", "components": ["zerossl"], "sources": ["zerossl:sddca28a3381b"], "status": "REASONED"}, + "acme-default": {"text": "acme.sh registers with ZeroSSL by default.", "components": ["acme"], "sources": ["acme:sa635c50fc1f9"], "status": "REASONED"}, + "acme-install": {"text": "acme.sh is a shell ACME client supporting many DNS providers; the guide recommends installation from a repository release instead of piping a download into a shell.", "components": ["acme"], "sources": ["acme:sa635c50fc1f9"], "status": "REASONED"}, + "server-automation": {"text": "This guide advises Caddy or Traefik for built-in issuance and renewal without an external client; it assesses them as the lowest-effort correct option for new deployments.", "components": ["le", "caddy", "traefik"], "sources": ["le:s9ff2d787c618", "caddy:s9683cb8d689f", "traefik:s418a6b1ad09b"], "status": "REASONED"}, + "origin-certificates": {"text": "Cloudflare lists Origin CA certificates on its Free plan, refers to long-lived origin CA certificates and says the validity period is chosen at creation, says they only encrypt traffic between Cloudflare and the origin, and warns that visitors may see untrusted certificate errors when Cloudflare is paused or proxying is disabled; this guide requires using them only for origins that receive traffic only through Cloudflare-proxied records.", "components": ["le", "cloudflare-origin-ca"], "sources": ["le:s9ff2d787c618", "cloudflare-origin-ca:s1e15de5c9f36"], "status": "REASONED"}, + "verify-inventory": {"text": "certbot certificates lists issued certificates and their expiry dates.", "components": ["certbot"], "sources": ["certbot:s44faaf4eb7d7"], "status": "REASONED", "verify": [1]}, + "verify-https": {"text": "Use the deployment's public hostname rather than example.com; curl HTTPS should succeed without -k.", "components": ["le", "curl-source"], "sources": ["le:s9ff2d787c618", "curl-source:s76f69702c9d5", "curl-source:s60820cd231e1"], "status": "REASONED", "verify": [1]}, + "verify-certificate": {"text": "The OpenSSL probe targets the hostname on port 443 with SNI, hostname verification and verification errors enabled, then displays issuer and dates.", "components": ["le", "openssl-cli"], "sources": ["le:s9ff2d787c618", "openssl-cli:s00aaf106164f", "openssl-cli:s0856f4a58574"], "status": "REASONED", "verify": [1]}, + "authentication": {"text": "A certificate alone is insufficient protection; continue with server controls and authentication.", "components": ["le"], "sources": ["le:s9ff2d787c618"], "status": "REASONED"}, + "dns-automation": {"text": "For automated DNS-01 issuance and renewal, this guide advises using a provider DNS plugin and least-privilege API credentials; Certbot also documents manual TXT entry and authentication hooks that automate the challenge.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s875be163b4c1", "certbot:s7b60a00cadf3", "certbot-v230:s55ec19f37395", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"}, + "dns-manual": {"text": "certbot certonly --manual --preferred-challenges dns permits hand-created TXT records; the Certbot v2.3.0 guide says certificates created with --manual do not support automatic renewal without a --manual-auth-hook that automates the challenge.", "components": ["certbot", "certbot-v230"], "sources": ["certbot:s7b60a00cadf3", "certbot-v230:s5bf4ff9fbe76"], "status": "REASONED"} + } +} +```` +====================================================================== +==> self-signed.md +====================================================================== + +# Self-signed certificates + +Written against OpenSSL documentation (rolling) (no recorded version), Requests TLS documentation v2.32.3, Git ignore documentation v2.43.0, curl command documentation curl-8_12_1, Node.js extra CA certificates v22.19.0, RFC 9525 service identity RFC 9525, Debian ca-certificates trixie, and Ubuntu ca-certificates noble, plus 3 more recorded bases; 1 further source carries no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 24 claims: 1 demonstrated by a check that ran and 23 reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + Use a self-signed certificate when the service has no public DNS name, when an ACME CA cannot reach it, and when [cloudflare.md](cloudflare.md) is not an option: internal tools, lab and development environments, and machine-to-machine links on private networks. For anything a browser user or external party reaches, prefer [free-certificates.md](free-certificates.md); self-signed certificates trigger browser warnings and every client must be configured to trust them. Self-signed TLS still matters. It encrypts credentials and data in transit; without it, authentication tokens cross the network in cleartext. @@ -841,6 +984,211 @@ Self-signed certificates have no revocation and no third-party accountability, a - Node.js v24.22.0 `tls.connect` client `rejectUnauthorized` (checked October 2026): https://github.com/nodejs/node/blob/v24.22.0/doc/api/tls.md#L1786-L1789 - Node.js v24.22.0 `tls.createServer` `rejectUnauthorized` and `requestCert` (checked October 2026): https://github.com/nodejs/node/blob/v24.22.0/doc/api/tls.md#L2250-L2255 + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/self-signed.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- openssl: OpenSSL documentation (rolling) (unknown) +- mkcert: mkcert (unknown) +- requests: Requests TLS documentation (v2.32.3) +- git: Git ignore documentation (v2.43.0) +- curl-source: curl command documentation (curl-8_12_1) +- node: Node.js extra CA certificates (v22.19.0) +- rfc9525: RFC 9525 service identity (RFC 9525) +- debian-ca: Debian ca-certificates (trixie) +- ubuntu-ca: Ubuntu ca-certificates (noble) +- rhel-ca: RHEL shared system certificates (RHEL 9) +- coreutils: GNU coreutils file permissions (v9.7) +- node-v24: Node.js API documentation (v24.22.0) + +Claims (full source mapping in the JSON block below): + +- trust-scope (REASONED; openssl, mkcert): Self-signed TLS encrypts transit but needs explicit client trust and triggers browser warnings; the guide prefers publicly trusted certificates for browser users and external parties. +- rsa-certificate (REASONED; openssl): OpenSSL 3.0 or later generates an RSA 4096-bit self-signed certificate with SHA-256, 365-day validity, an unencrypted key and the shown subject and extensions. +- ec-certificate (REASONED; openssl): Generate a prime256v1 EC key first, then a SHA-256 self-signed certificate with 365-day validity and the shown subject and extensions; the guide describes ECDSA as smaller and faster. +- san (REASONED; openssl, rfc9525): SAN must cover every client DNS name and IP (this guide's requirement, based on RFC 9525's matching rules); replace the documentation IP or omit IP entries for name-only access. RFC 9525 requires DNS identity matching label by label except under its wildcard rule; an IP-ID reference identifier MUST exactly match an iPAddress subjectAltName entry. It forbids the subject Common Name as a service identifier. On mismatch, an automated client SHOULD terminate; a configured exception MUST NOT disable this control by default. A human-facing client SHOULD warn and terminate but MAY offer advanced users an override. Ad hoc pinning SHOULD NOT restrict future connections to only the pinned certificate. +- leaf-constraint (REASONED; openssl): Set basicConstraints=critical,CA:FALSE because the stated stock req -x509 configuration otherwise marks a trusted certificate CA:TRUE, allowing its unencrypted server key to sign for any name. +- extension-history (REASONED; openssl): The command-line extension override relies on OpenSSL 3.0 or later; the guide says end-of-life 1.1.1 instead emits a duplicate invalid extension. +- unencrypted-key (REASONED; openssl): -noenc leaves the key unencrypted for unattended startup; -nodes is the pre-3.0 spelling, still accepted but deprecated. Protect the key with permissions. +- expiry (REASONED; openssl): Track the -days expiry with a calendar or monitoring; self-signed certificates do not renew themselves. +- key-permissions (REASONED; openssl, coreutils): This guide advises chmod 600 and service-user ownership for server.key; GNU coreutils v9.7 documents chmod's mode changes and chown's owner/group changes, not that advice. Mode 600's owner read/write and no group/other permissions are this guide's reading of GNU coreutils v9.7's Numeric Modes definition in doc/perm.texi. +- key-compromise (REASONED; openssl, git): Never commit private keys; ignore *.key and *.pem before generation in a repository and regenerate keys committed or pasted into chat. +- mkcert (REASONED; mkcert): mkcert -install creates and trusts a local CA; mkcert issues certificates for the listed development DNS names and IPs. +- mkcert-custody (REASONED; mkcert): Use mkcert only for development; its CA key can sign for any name, must not leave the developer's machine and must not serve real users. +- debian-trust (REASONED; mkcert, debian-ca, ubuntu-ca): Install app-internal.crt under /usr/local/share/ca-certificates and run update-ca-certificates on Debian trixie and Ubuntu noble; use on other Debian and Ubuntu releases is this guide's assessment, with platform-specific Sources entries for those releases absent. +- rhel-trust (REASONED; mkcert, rhel-ca): Install app-internal.crt under /etc/pki/ca-trust/source/anchors and run update-ca-trust extract on RHEL 9; Fedora and other RHEL releases are this guide's assessment, with platform-specific Sources entries for those releases absent. +- curl-trust (REASONED; openssl, curl-source): curl --cacert server.crt supplies explicit trust for app.internal. +- requests-trust (REASONED; openssl, requests): REQUESTS_CA_BUNDLE supplies the certificate path to Python requests. +- node-trust (REASONED; mkcert, node): NODE_EXTRA_CA_CERTS supplies additional client trust to Node.js. +- verification-bypasses (REASONED; openssl, requests, curl-source, node-v24): Do not commit curl -k, verify=False, rejectUnauthorized: false or NODE_TLS_REJECT_UNAUTHORIZED=0. Node.js v24.22.0 scopes the bypasses to process-environment TLS validation, client-connection server verification or server rejection with requestCert=true; accepting attacker certificates and persistence into production are this guide's assessment. +- verify-extensions (DEMONSTRATED; openssl): Inspect x509 output for SAN coverage and CA:FALSE, not its exit status: it can exit 0 with no SAN. +- verify-tls (REASONED; openssl): The app.internal:443 s_client check supplies SNI, hostname checking, verification errors and CAfile; a pass prints Verification: OK, Verified peername: app.internal and return code 0, and exits 0. +- verify-failures (REASONED; openssl): Without -verify_hostname the name is ignored; without -verify_return_error a verification failure can still complete the handshake and exit 0. Both flags make name or trust failures exit nonzero; wrong name reports code 62. +- verify-identity (REASONED; openssl): Use -verify_ip for IP identities; -verify_hostname can fall back to CN without a DNS SAN, even with an IP SAN, so CN-only or IP-only certificates can pass hostname checks. +- verify-store (REASONED; openssl, mkcert, curl-source): Explicit -CAfile proves certificate and name, not the system-store installation; test each normally configured client separately, expecting plain curl without --cacert to fail before trust installation and succeed after. +- trust-limits (REASONED; openssl, mkcert): Self-signed certificates have no revocation or third-party accountability and need manual trust distribution; move beyond them as the audience grows and retain authentication. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "b209ce0aa20390b1ec8888e436f6bb87ea31f15f19bcd4d8529c14faca6a432a", + "components": { + "openssl": { + "name": "OpenSSL documentation (rolling)", + "basis": "unknown", + "sources": { + "s211b1e6e4aa4": "https://docs.openssl.org/", + "s581d7ad32359": "https://docs.openssl.org/master/man1/openssl-s_client/", + "se5d70578564e": "https://docs.openssl.org/master/man1/openssl-verification-options/" + } + }, + "mkcert": { + "name": "mkcert", + "basis": "unknown", + "sources": { + "s41ae17dba4ce": "https://github.com/FiloSottile/mkcert" + } + }, + "requests": { + "name": "Requests TLS documentation", + "basis": "v2.32.3", + "sources": { + "s21c6be6f6df4": "https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L238-L239", + "scff02f87cf30": "https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L246-L250" + } + }, + "git": { + "name": "Git ignore documentation", + "basis": "v2.43.0", + "sources": { + "sf961a2e5b8e1": "https://github.com/git/git/blob/v2.43.0/Documentation/gitignore.txt#L15-L18" + } + }, + "curl-source": { + "name": "curl command documentation", + "basis": "curl-8_12_1", + "sources": { + "s19ed21fc98f8": "https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/cacert.md#L21-L24", + "s60820cd231e1": "https://github.com/curl/curl/blob/curl-8_12_1/docs/cmdline-opts/insecure.md#L21-L28" + } + }, + "node": { + "name": "Node.js extra CA certificates", + "basis": "v22.19.0", + "sources": { + "s402034f5dcea": "https://github.com/nodejs/node/blob/v22.19.0/doc/api/cli.md#L3234-L3238" + } + }, + "rfc9525": { + "name": "RFC 9525 service identity", + "basis": "RFC 9525", + "sources": { + "s8dcb5fcc7802": "https://www.rfc-editor.org/rfc/rfc9525.html#section-2", + "s4afb46847ce6": "https://www.rfc-editor.org/rfc/rfc9525.html#section-6.2", + "s31c00ddbdfa5": "https://www.rfc-editor.org/rfc/rfc9525.html#section-6.3", + "s416ff7327342": "https://www.rfc-editor.org/rfc/rfc9525.html#section-6.4", + "s8bb6a7aa9400": "https://www.rfc-editor.org/rfc/rfc9525.html#section-6.6" + } + }, + "debian-ca": { + "name": "Debian ca-certificates", + "basis": "trixie", + "sources": { + "s99e100121493": "https://manpages.debian.org/trixie/ca-certificates/update-ca-certificates.8.en.html#DESCRIPTION" + } + }, + "ubuntu-ca": { + "name": "Ubuntu ca-certificates", + "basis": "noble", + "sources": { + "s4c3728cc6c12": "https://manpages.ubuntu.com/manpages/noble/man8/update-ca-certificates.8.html#description" + } + }, + "rhel-ca": { + "name": "RHEL shared system certificates", + "basis": "RHEL 9", + "sources": { + "s876e668e7116": "https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/securing_networks/using-shared-system-certificates_securing-networks#adding-new-certificates_using-shared-system-certificates" + } + }, + "coreutils": { + "name": "GNU coreutils file permissions", + "basis": "v9.7", + "sources": { + "s876896563d01": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11937-L11942", + "s881033f651b3": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11967-L11968", + "s9ab719f6189b": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L11614-L11622", + "s0ef955010eb3": "https://github.com/coreutils/coreutils/blob/v9.7/doc/perm.texi#L492-L535" + } + }, + "node-v24": { + "name": "Node.js API documentation", + "basis": "v24.22.0", + "sources": { + "sbfee17929443": "https://github.com/nodejs/node/blob/v24.22.0/doc/api/cli.md#L3940-L3944", + "s684a71cf8b6e": "https://github.com/nodejs/node/blob/v24.22.0/doc/api/tls.md#L1786-L1789", + "s025a5157269c": "https://github.com/nodejs/node/blob/v24.22.0/doc/api/tls.md#L2250-L2255" + } + } + }, + "claims": { + "trust-scope": {"text": "Self-signed TLS encrypts transit but needs explicit client trust and triggers browser warnings; the guide prefers publicly trusted certificates for browser users and external parties.", "components": ["openssl", "mkcert"], "sources": ["openssl:s211b1e6e4aa4", "mkcert:s41ae17dba4ce"], "status": "REASONED"}, + "rsa-certificate": {"text": "OpenSSL 3.0 or later generates an RSA 4096-bit self-signed certificate with SHA-256, 365-day validity, an unencrypted key and the shown subject and extensions.", "components": ["openssl"], "sources": ["openssl:s211b1e6e4aa4"], "status": "REASONED"}, + "ec-certificate": {"text": "Generate a prime256v1 EC key first, then a SHA-256 self-signed certificate with 365-day validity and the shown subject and extensions; the guide describes ECDSA as smaller and faster.", "components": ["openssl"], "sources": ["openssl:s211b1e6e4aa4"], "status": "REASONED"}, + "san": {"text": "SAN must cover every client DNS name and IP (this guide's requirement, based on RFC 9525's matching rules); replace the documentation IP or omit IP entries for name-only access. RFC 9525 requires DNS identity matching label by label except under its wildcard rule; an IP-ID reference identifier MUST exactly match an iPAddress subjectAltName entry. It forbids the subject Common Name as a service identifier. On mismatch, an automated client SHOULD terminate; a configured exception MUST NOT disable this control by default. A human-facing client SHOULD warn and terminate but MAY offer advanced users an override. Ad hoc pinning SHOULD NOT restrict future connections to only the pinned certificate.", "components": ["openssl", "rfc9525"], "sources": ["openssl:se5d70578564e", "rfc9525:s8dcb5fcc7802", "rfc9525:s4afb46847ce6", "rfc9525:s31c00ddbdfa5", "rfc9525:s416ff7327342", "rfc9525:s8bb6a7aa9400"], "status": "REASONED"}, + "leaf-constraint": {"text": "Set basicConstraints=critical,CA:FALSE because the stated stock req -x509 configuration otherwise marks a trusted certificate CA:TRUE, allowing its unencrypted server key to sign for any name.", "components": ["openssl"], "sources": ["openssl:s211b1e6e4aa4"], "status": "REASONED"}, + "extension-history": {"text": "The command-line extension override relies on OpenSSL 3.0 or later; the guide says end-of-life 1.1.1 instead emits a duplicate invalid extension.", "components": ["openssl"], "sources": ["openssl:s211b1e6e4aa4"], "status": "REASONED"}, + "unencrypted-key": {"text": "-noenc leaves the key unencrypted for unattended startup; -nodes is the pre-3.0 spelling, still accepted but deprecated. Protect the key with permissions.", "components": ["openssl"], "sources": ["openssl:s211b1e6e4aa4"], "status": "REASONED"}, + "expiry": {"text": "Track the -days expiry with a calendar or monitoring; self-signed certificates do not renew themselves.", "components": ["openssl"], "sources": ["openssl:s211b1e6e4aa4"], "status": "REASONED"}, + "key-permissions": {"text": "This guide advises chmod 600 and service-user ownership for server.key; GNU coreutils v9.7 documents chmod's mode changes and chown's owner/group changes, not that advice. Mode 600's owner read/write and no group/other permissions are this guide's reading of GNU coreutils v9.7's Numeric Modes definition in doc/perm.texi.", "components": ["openssl", "coreutils"], "sources": ["openssl:s211b1e6e4aa4", "coreutils:s876896563d01", "coreutils:s881033f651b3", "coreutils:s9ab719f6189b", "coreutils:s0ef955010eb3"], "status": "REASONED"}, + "key-compromise": {"text": "Never commit private keys; ignore *.key and *.pem before generation in a repository and regenerate keys committed or pasted into chat.", "components": ["openssl", "git"], "sources": ["openssl:s211b1e6e4aa4", "git:sf961a2e5b8e1"], "status": "REASONED"}, + "mkcert": {"text": "mkcert -install creates and trusts a local CA; mkcert issues certificates for the listed development DNS names and IPs.", "components": ["mkcert"], "sources": ["mkcert:s41ae17dba4ce"], "status": "REASONED"}, + "mkcert-custody": {"text": "Use mkcert only for development; its CA key can sign for any name, must not leave the developer's machine and must not serve real users.", "components": ["mkcert"], "sources": ["mkcert:s41ae17dba4ce"], "status": "REASONED"}, + "debian-trust": {"text": "Install app-internal.crt under /usr/local/share/ca-certificates and run update-ca-certificates on Debian trixie and Ubuntu noble; use on other Debian and Ubuntu releases is this guide's assessment, with platform-specific Sources entries for those releases absent.", "components": ["mkcert", "debian-ca", "ubuntu-ca"], "sources": ["mkcert:s41ae17dba4ce", "debian-ca:s99e100121493", "ubuntu-ca:s4c3728cc6c12"], "status": "REASONED"}, + "rhel-trust": {"text": "Install app-internal.crt under /etc/pki/ca-trust/source/anchors and run update-ca-trust extract on RHEL 9; Fedora and other RHEL releases are this guide's assessment, with platform-specific Sources entries for those releases absent.", "components": ["mkcert", "rhel-ca"], "sources": ["mkcert:s41ae17dba4ce", "rhel-ca:s876e668e7116"], "status": "REASONED"}, + "curl-trust": {"text": "curl --cacert server.crt supplies explicit trust for app.internal.", "components": ["openssl", "curl-source"], "sources": ["openssl:se5d70578564e", "curl-source:s19ed21fc98f8"], "status": "REASONED"}, + "requests-trust": {"text": "REQUESTS_CA_BUNDLE supplies the certificate path to Python requests.", "components": ["openssl", "requests"], "sources": ["openssl:se5d70578564e", "requests:s21c6be6f6df4"], "status": "REASONED"}, + "node-trust": {"text": "NODE_EXTRA_CA_CERTS supplies additional client trust to Node.js.", "components": ["mkcert", "node"], "sources": ["mkcert:s41ae17dba4ce", "node:s402034f5dcea"], "status": "REASONED"}, + "verification-bypasses": {"text": "Do not commit curl -k, verify=False, rejectUnauthorized: false or NODE_TLS_REJECT_UNAUTHORIZED=0. Node.js v24.22.0 scopes the bypasses to process-environment TLS validation, client-connection server verification or server rejection with requestCert=true; accepting attacker certificates and persistence into production are this guide's assessment.", "components": ["openssl", "requests", "curl-source", "node-v24"], "sources": ["openssl:se5d70578564e", "requests:scff02f87cf30", "curl-source:s60820cd231e1", "node-v24:sbfee17929443", "node-v24:s684a71cf8b6e", "node-v24:s025a5157269c"], "status": "REASONED"}, + "verify-extensions": {"text": "Inspect x509 output for SAN coverage and CA:FALSE, not its exit status: it can exit 0 with no SAN.", "components": ["openssl"], "sources": ["openssl:s211b1e6e4aa4"], "status": "DEMONSTRATED", "evidence": "On 2026-10-05 in private /dev/shm scratch, OpenSSL 3.5.5 ran the printed x509 command on three disposable one-day RSA certificates. Missing-SAN and CA:TRUE fixtures each exited 0 but lacked the required SAN or leaf constraint; the fixed fixture also exited 0 and printed DNS:app.internal, IP Address:127.0.0.1 and critical CA:FALSE. Subject and validity dates printed for all three.", "verify": [1]}, + "verify-tls": {"text": "The app.internal:443 s_client check supplies SNI, hostname checking, verification errors and CAfile; a pass prints Verification: OK, Verified peername: app.internal and return code 0, and exits 0.", "components": ["openssl"], "sources": ["openssl:s581d7ad32359", "openssl:se5d70578564e"], "status": "REASONED", "verify": [2]}, + "verify-failures": {"text": "Without -verify_hostname the name is ignored; without -verify_return_error a verification failure can still complete the handshake and exit 0. Both flags make name or trust failures exit nonzero; wrong name reports code 62.", "components": ["openssl"], "sources": ["openssl:s581d7ad32359", "openssl:se5d70578564e"], "status": "REASONED", "verify": [2]}, + "verify-identity": {"text": "Use -verify_ip for IP identities; -verify_hostname can fall back to CN without a DNS SAN, even with an IP SAN, so CN-only or IP-only certificates can pass hostname checks.", "components": ["openssl"], "sources": ["openssl:se5d70578564e"], "status": "REASONED", "verify": [2]}, + "verify-store": {"text": "Explicit -CAfile proves certificate and name, not the system-store installation; test each normally configured client separately, expecting plain curl without --cacert to fail before trust installation and succeed after.", "components": ["openssl", "mkcert", "curl-source"], "sources": ["openssl:s581d7ad32359", "mkcert:s41ae17dba4ce", "curl-source:s19ed21fc98f8", "curl-source:s60820cd231e1"], "status": "REASONED", "verify": [2]}, + "trust-limits": {"text": "Self-signed certificates have no revocation or third-party accountability and need manual trust distribution; move beyond them as the audience grows and retain authentication.", "components": ["openssl", "mkcert"], "sources": ["openssl:s211b1e6e4aa4", "mkcert:s41ae17dba4ce"], "status": "REASONED"} + } +} +```` + + ====================================================================== ==> cloudflare.md ====================================================================== @@ -5021,53 +5369,9 @@ REASONED: preceding block; listener isolation, anonymous challenges and direct a # Apache HTTP Server: TLS and authentication - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-18; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- apache: Apache HTTP Server (2.4) -- policy: Mozilla TLS policy (unknown) -- tls-min: Apache TLSv1.3 minimum (2.4.36+) -- sni-min: Apache per-vhost protocol minimum (2.4.42+) -- openssl: OpenSSL qualification (1.1.1+) -- chain-min: Apache chain-file minimum (2.4.8) -- certbot: Certbot Apache installation (v2.3.0) -- debian-packaging: Debian apache2 packaging (2.4.69-1) -- fedora-packaging: Fedora f44 httpd packaging (commit 6df70d39) -- htpasswd: httpd htpasswd (2.4.69) -- owasp: OWASP password storage cheat sheet (commit 72865b1e) -- authelia: Authelia proxy support (v4.39.28) -- cloudflare-access: Cloudflare Access documentation (commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- openidc: mod_auth_openidc (v2.4.20.3) - -Claims (full source mapping in the front matter): +Written against Apache HTTP Server 2.4, Apache TLSv1.3 minimum 2.4.36+, Apache per-vhost protocol minimum 2.4.42+, Apache chain-file minimum 2.4.8, httpd htpasswd 2.4.69, OpenSSL qualification 1.1.1+, Certbot Apache installation v2.3.0, and Debian apache2 packaging 2.4.69-1, plus 5 more recorded bases; 1 further source carries no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 20 claims, all reasoned from cited sources. -- certbot (REASONED; apache, certbot): The guide says certbot --apache installs certificates and redirects by default; --redirect is explicit and --hsts defaults off. -- modules (REASONED; apache, debian-packaging, fedora-packaging): Enable ssl/headers and a 443 vhost on Debian/Ubuntu, or install mod_ssl/httpd-tools on RHEL/Fedora; pinned packaging shows a2enmod, a2ensite (a link to a2enmod), default-ssl, and Fedora package names. -- tls (REASONED; apache): The 443 vhost enables TLS with a certificate chain and private key and TLSv1.2/TLSv1.3 only. -- tls-version (REASONED; apache, tls-min, openssl): TLSv1.3 needs Apache 2.4.36+ with OpenSSL 1.1.1+; older builds use SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1. -- vhost-floor (REASONED; apache, sni-min, openssl): Independent per-vhost SSLProtocol needs Apache 2.4.42+, OpenSSL 1.1.1+ and client SNI; older builds also need the floor in global/base config. -- chain (REASONED; apache, chain-min): Apache 2.4.8+ permits the chain in SSLCertificateFile; SSLCertificateChainFile is deprecated. -- hsts (REASONED; apache): Header always sets HSTS after HTTPS works; includeSubDomains requires valid HTTPS everywhere. -- cipher-policy (REASONED; policy): Generate explicit cipher policy with Mozilla rather than copying old lists. -- redirect (REASONED; apache): The 80 vhost sends a permanent redirect to the canonical HTTPS host; retain only redirects and needed ACME HTTP-01 paths. -- password-file (REASONED; apache, debian-packaging, fedora-packaging, htpasswd, owasp): Create a bcrypt htpasswd file with cost 12 and -c only for its first user; httpd 2.4.69 defaults bcrypt cost to 5 and OWASP recommends at least 10 for legacy bcrypt. Both password-file paths are guide choices. -- basic (REASONED; apache): AuthType Basic, AuthName, AuthUserFile and Require valid-user protect the chosen site/path over TLS; enumerate every public path. -- mtls (REASONED; apache): A separate machine-client 443 vhost requires a trusted certificate with SSLVerifyClient require and depth 2; SSLCACertificateFile is server/vhost scoped. -- mfa (REASONED; apache, authelia, openidc, cloudflare-access): Basic is single-factor; Authelia v4.39.28 says Apache is unsupported for its proxy integration, and mod_auth_openidc v2.4.20.3 is an OIDC relying party. Provider MFA is guide advice; Cloudflare Access documents self-hosted application and MFA policy controls; fronting an Apache origin is this guide's suggested option and assessment. This guide assesses that an Apache origin reachable without passing through Access is not protected by it. -- key-permissions (REASONED; apache): Keep private keys root-owned and mode 0600. -- verify-config (REASONED; apache): Run apachectl configtest before service reload; no outcome is recorded. -- verify-redirect (REASONED; apache): HTTP should return 301 with an HTTPS Location. -- verify-auth (REASONED; apache): Read printed status: curl can exit 0 for 200 or 401; expect 200 before auth and 401 after it. -- verify-listeners (REASONED; apache): Inspect the entire listener table, without filtering away unexpected ports. -- verify-unmatched (REASONED; apache): Use an unconfigured certificate-covered URL name for both SNI and Host; 401 passes, 200 exposes a protected resource through another vhost. Host-only probes can misleadingly give 401 or 421. -- verify-vhost-review (REASONED; apache): If no spare certificate-covered name exists, use apachectl -S and review that the first matching vhost cannot serve the protected DocumentRoot. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Applies to Apache 2.4. Get a certificate first: [free-certificates.md](free-certificates.md) for a public host (note that `certbot --apache` obtains and installs the certificate and, at the time of writing, enables the HTTP-to-HTTPS redirect by default (`--redirect` requests it explicitly), but review its generated TLS config against steps 1 to 3 and add HSTS yourself, since certbot's `--hsts` is off by default), or [self-signed.md](self-signed.md) for internal use. @@ -5226,65 +5530,235 @@ sudo apachectl -S # the vhost list, in Apache's own matchi - Cloudflare Access MFA policy controls at commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81 - mod_auth_openidc v2.4.20.3 relying-party description: https://github.com/OpenIDC/mod_auth_openidc/blob/v2.4.20.3/README.md#L11-L20 -====================================================================== -==> nginx.md -====================================================================== - -# nginx: TLS and authentication +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/apache.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + **Version basis** -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. -Metadata reviewed 2026-10-17; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- ng: nginx documentation (unknown) +- apache: Apache HTTP Server (2.4) +- tls-min: Apache TLSv1.3 minimum (2.4.36+) +- sni-min: Apache per-vhost protocol minimum (2.4.42+) +- chain-min: Apache chain-file minimum (2.4.8) +- htpasswd: httpd htpasswd (2.4.69) +- openssl: OpenSSL qualification (1.1.1+) +- certbot: Certbot Apache installation (v2.3.0) +- debian-packaging: Debian apache2 packaging (2.4.69-1) +- fedora-packaging: Fedora f44 httpd packaging (commit 6df70d39) +- authelia: Authelia proxy support (v4.39.28) +- cloudflare-access: Cloudflare Access documentation (commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- openidc: mod_auth_openidc (v2.4.20.3) - policy: Mozilla TLS policy (unknown) -- certbot: Certbot nginx configurator (v2.3.0) -- hsts: HSTS specification (RFC 6797) -- nginx-source: nginx source (release-1.26.3) -- authelia: Authelia nginx integration (v4.39.28) -- oauth2-proxy: oauth2-proxy nginx integration (v7.15.4) -- cloudflare-access: Cloudflare Access web applications (unknown) -- httpd: Apache httpd source (2.4.69) -- debian-apache2: Debian apache2 packaging (2.4.69-1) -- owasp: OWASP Password Storage Cheat Sheet (commit 72865b1e) +- owasp: OWASP password storage cheat sheet (commit 72865b1e) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- certbot (REASONED; ng, certbot): The guide says certbot --nginx edits the server block; certificate acquisition is delegated to the linked certificate guides. -- tls (REASONED; ng): 443 IPv4/IPv6 TLS uses certificate chain and private key, TLSv1.2/TLSv1.3 and ssl_prefer_server_ciphers off. -- http2 (REASONED; ng, nginx-source): At nginx release-1.26.3, http2 on is the HTTP/2 module directive; nginx 1.25.1 introduced it and deprecated the older listen 443 ssl http2 parameter. -- tokens (REASONED; ng): server_tokens off suppresses nginx version in headers and error pages. -- hsts (REASONED; ng, hsts, nginx-source): Send HSTS only after HTTPS works; includeSubDomains commits every subdomain for max-age. -- hsts-inheritance (REASONED; ng, nginx-source): At nginx release-1.26.3, a location add_header replaces inherited server headers; always changes response-code coverage, not inheritance. -- proxy (REASONED; ng): Proxy to loopback 127.0.0.1:3000 and set Host, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host. -- forwarded-host (REASONED; ng): Overwrite X-Forwarded-Host: client-set values otherwise pass upstream. -- cipher-policy (REASONED; ng, policy): Use Mozilla for explicit cipher policy; retain the stated protocol floor. -- tickets (REASONED; ng, nginx-source): At nginx release-1.26.3, ssl_session_tickets defaults on; nginx 1.23.2 introduced automatic ticket-key rotation with a shared ssl_session_cache. -- redirect (REASONED; ng): Port 80 on IPv4/IPv6 redirects with 301; a default server reflecting $host can redirect unmatched hosts off-site, so use 444 or a canonical host. -- password-file (REASONED; ng, httpd, owasp, debian-apache2): At httpd 2.4.69, htpasswd -B selects bcrypt, bare -B uses cost 5, and -C sets the cost; the OWASP cheat sheet at commit 72865b1e recommends at least 10 for legacy bcrypt. Debian apache2 2.4.69-1 packages htpasswd in apache2-utils. -- basic (REASONED; ng): auth_basic and auth_basic_user_file gate the existing proxied location over TLS; preserve its forwarded headers. -- mtls (REASONED; ng, nginx-source): At nginx release-1.26.3, ssl_client_certificate and ssl_verify_client on configure trusted client-certificate verification. -- mfa (REASONED; ng, nginx-source, authelia, oauth2-proxy, cloudflare-access): At nginx release-1.26.3, auth_request permits 2xx authentication subresponses and returns 401/403; Authelia v4.39.28 and oauth2-proxy v7.15.4 document nginx auth_request integration, and Cloudflare Access documents protecting self-hosted web applications; the nginx source cited here does not cover those integrations. MFA is this guide's requirement; whether a given installation enforces it depends on its configured portal or Access policy (this guide's assessment). -- body-limit (REASONED; ng): client_max_body_size defaults to 1m; 10m rejects larger bodies with 413, and 0 disables the check. -- rate-limit (REASONED; ng): Per-address limit_req uses a 10r/s zone, burst 20 and nodelay; rejection status defaults to 503. -- connection-limit (REASONED; ng): limit_conn apiconn 10 caps concurrent connections per client address; it can also return 503. -- location-auth (REASONED; ng): Keep limits in the authenticated location; sibling locations do not inherit its auth or proxy headers. Repeat auth or inherit it from server; auth_basic off is deliberate. -- read-timeout (REASONED; ng): proxy_read_timeout 60s limits gaps between upstream reads, not total response duration; active streams can continue indefinitely. -- client-address (REASONED; ng): At a direct edge trust the appended last X-Forwarded-For entry; behind a CDN, recover the client with trusted set_real_ip_from and real_ip_header before per-client limits. -- verify-config (REASONED; ng): Run nginx -t before reload; no configuration-test outcome is recorded. -- verify-redirect (REASONED; ng): HTTP should return 301 with an HTTPS Location. -- verify-tls (REASONED; ng): HTTPS must validate without -k; TLS 1.1-only rejection must be protocol_version, not a generic/local failure; test TLS 1.0 separately. -- verify-auth (REASONED; ng): An open path may return 200; the protected path must reject no credentials with 401/403, and valid credentials reach the app rather than 401. -- verify-size (REASONED; ng): Authenticated 9M succeeds normally only after raising the 1m default; 11M returns 413. Disable the check with 0 in isolation to distinguish backend refusal. -- verify-rate (REASONED; ng): Concurrent unauthenticated probes exercise pre-auth limits; 503 does not identify the limiter. Compare both off and each alone with measured arrival rate. -- verify-backend (REASONED; ng): Inspect every listener: direct public access to backend 3000 bypasses proxy TLS/auth even when front-door probes pass. +- certbot (REASONED; apache, certbot): The guide says certbot --apache installs certificates and redirects by default; --redirect is explicit and --hsts defaults off. +- modules (REASONED; apache, debian-packaging, fedora-packaging): Enable ssl/headers and a 443 vhost on Debian/Ubuntu, or install mod_ssl/httpd-tools on RHEL/Fedora; pinned packaging shows a2enmod, a2ensite (a link to a2enmod), default-ssl, and Fedora package names. +- tls (REASONED; apache): The 443 vhost enables TLS with a certificate chain and private key and TLSv1.2/TLSv1.3 only. +- tls-version (REASONED; apache, tls-min, openssl): TLSv1.3 needs Apache 2.4.36+ with OpenSSL 1.1.1+; older builds use SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1. +- vhost-floor (REASONED; apache, sni-min, openssl): Independent per-vhost SSLProtocol needs Apache 2.4.42+, OpenSSL 1.1.1+ and client SNI; older builds also need the floor in global/base config. +- chain (REASONED; apache, chain-min): Apache 2.4.8+ permits the chain in SSLCertificateFile; SSLCertificateChainFile is deprecated. +- hsts (REASONED; apache): Header always sets HSTS after HTTPS works; includeSubDomains requires valid HTTPS everywhere. +- cipher-policy (REASONED; policy): Generate explicit cipher policy with Mozilla rather than copying old lists. +- redirect (REASONED; apache): The 80 vhost sends a permanent redirect to the canonical HTTPS host; retain only redirects and needed ACME HTTP-01 paths. +- password-file (REASONED; apache, debian-packaging, fedora-packaging, htpasswd, owasp): Create a bcrypt htpasswd file with cost 12 and -c only for its first user; httpd 2.4.69 defaults bcrypt cost to 5 and OWASP recommends at least 10 for legacy bcrypt. Both password-file paths are guide choices. +- basic (REASONED; apache): AuthType Basic, AuthName, AuthUserFile and Require valid-user protect the chosen site/path over TLS; enumerate every public path. +- mtls (REASONED; apache): A separate machine-client 443 vhost requires a trusted certificate with SSLVerifyClient require and depth 2; SSLCACertificateFile is server/vhost scoped. +- mfa (REASONED; apache, authelia, openidc, cloudflare-access): Basic is single-factor; Authelia v4.39.28 says Apache is unsupported for its proxy integration, and mod_auth_openidc v2.4.20.3 is an OIDC relying party. Provider MFA is guide advice; Cloudflare Access documents self-hosted application and MFA policy controls; fronting an Apache origin is this guide's suggested option and assessment. This guide assesses that an Apache origin reachable without passing through Access is not protected by it. +- key-permissions (REASONED; apache): Keep private keys root-owned and mode 0600. +- verify-config (REASONED; apache): Run apachectl configtest before service reload; no outcome is recorded. +- verify-redirect (REASONED; apache): HTTP should return 301 with an HTTPS Location. +- verify-auth (REASONED; apache): Read printed status: curl can exit 0 for 200 or 401; expect 200 before auth and 401 after it. +- verify-listeners (REASONED; apache): Inspect the entire listener table, without filtering away unexpected ports. +- verify-unmatched (REASONED; apache): Use an unconfigured certificate-covered URL name for both SNI and Host; 401 passes, 200 exposes a protected resource through another vhost. Host-only probes can misleadingly give 401 or 421. +- verify-vhost-review (REASONED; apache): If no spare certificate-covered name exists, use apachectl -S and review that the first matching vhost cannot serve the protected DocumentRoot. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "645f3f5ab27a4df2af69e0680ac8e98f85ca416fe85a078c95075598e9b33f4d", + "components": { + "apache": { + "name": "Apache HTTP Server", + "basis": "2.4", + "sources": { + "sde155f1667f3": "https://httpd.apache.org/docs/2.4/ssl/ssl_howto.html", + "s99dcb49d86a4": "https://httpd.apache.org/docs/2.4/howto/auth.html", + "sc07f97af99e3": "https://httpd.apache.org/docs/2.4/vhosts/name-based.html", + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy", + "s7c302c734a0d": "https://httpd.apache.org/docs/2.4/vhosts/details.html", + "s1dbb2636d050": "https://httpd.apache.org/docs/2.4/mod/mod_headers.html" + } + }, + "tls-min": { + "name": "Apache TLSv1.3 minimum", + "basis": "2.4.36+", + "sources": { + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" + } + }, + "sni-min": { + "name": "Apache per-vhost protocol minimum", + "basis": "2.4.42+", + "sources": { + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" + } + }, + "chain-min": { + "name": "Apache chain-file minimum", + "basis": "2.4.8", + "sources": { + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" + } + }, + "htpasswd": { + "name": "httpd htpasswd", + "basis": "2.4.69", + "sources": { + "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128", + "s7850e1e9dfa1": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L43-L45", + "s1c1a101a70e7": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L290-L295" + } + }, + "openssl": { + "name": "OpenSSL qualification", + "basis": "1.1.1+", + "sources": { + "scf804f4940d7": "https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslvhostsnipolicy" + } + }, + "certbot": { + "name": "Certbot Apache installation", + "basis": "v2.3.0", + "sources": { + "sc74fcd0167d8": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L104-L106", + "sa453a66e4130": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/client.py#L646", + "sacf90ffa5a6d": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/client.py#L674-L677", + "s74412ad87016": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/constants.py#L66-L68" + } + }, + "debian-packaging": { + "name": "Debian apache2 packaging", + "basis": "2.4.69-1", + "sources": { + "s1c05297f1ef0": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.install#L1-7", + "s32bd234fb40a": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/config-dir/sites-available/default-ssl.conf#L1-4", + "sc9b152b1a444": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.links#L7", + "sd1c9de6b39aa": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/control#L92-102", + "s62f6ed2866ab": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1" + } + }, + "fedora-packaging": { + "name": "Fedora f44 httpd packaging", + "basis": "commit 6df70d39", + "sources": { + "s7d73f140a3fe": "https://src.fedoraproject.org/rpms/httpd/blob/6df70d392dace11a7c01ceffc5d8a929de0b7a35/f/httpd.spec#_204-212", + "s4479577a2e6a": "https://src.fedoraproject.org/rpms/httpd/blob/6df70d392dace11a7c01ceffc5d8a929de0b7a35/f/httpd.spec#_819-824" + } + }, + "authelia": { + "name": "Authelia proxy support", + "basis": "v4.39.28", + "sources": { + "s1de7fe72d011": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L112-L115" + } + }, + "cloudflare-access": { + "name": "Cloudflare Access documentation", + "basis": "commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "sfddb92e438ca": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L14-L16", + "s7cafe1b17afd": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54", + "s603bdb6b24ab": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L45-L45", + "s863803bd9a1f": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L48-L48", + "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" + } + }, + "openidc": { + "name": "mod_auth_openidc", + "basis": "v2.4.20.3", + "sources": { + "s01e183d16f03": "https://github.com/OpenIDC/mod_auth_openidc/blob/v2.4.20.3/README.md#L11-L20" + } + }, + "policy": { + "name": "Mozilla TLS policy", + "basis": "unknown", + "sources": { + "s529b0eabe2ed": "https://ssl-config.mozilla.org/" + } + }, + "owasp": { + "name": "OWASP password storage cheat sheet", + "basis": "commit 72865b1e", + "sources": { + "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" + } + } + }, + "claims": { + "certbot": {"text": "The guide says certbot --apache installs certificates and redirects by default; --redirect is explicit and --hsts defaults off.", "components": ["apache", "certbot"], "sources": ["apache:sde155f1667f3", "certbot:sc74fcd0167d8", "certbot:sa453a66e4130", "certbot:sacf90ffa5a6d", "certbot:s74412ad87016"], "status": "REASONED"}, + "modules": {"text": "Enable ssl/headers and a 443 vhost on Debian/Ubuntu, or install mod_ssl/httpd-tools on RHEL/Fedora; pinned packaging shows a2enmod, a2ensite (a link to a2enmod), default-ssl, and Fedora package names.", "components": ["apache", "debian-packaging", "fedora-packaging"], "sources": ["apache:sde155f1667f3", "debian-packaging:s62f6ed2866ab", "debian-packaging:s1c05297f1ef0", "debian-packaging:sc9b152b1a444", "debian-packaging:s32bd234fb40a", "fedora-packaging:s7d73f140a3fe"], "status": "REASONED"}, + "tls": {"text": "The 443 vhost enables TLS with a certificate chain and private key and TLSv1.2/TLSv1.3 only.", "components": ["apache"], "sources": ["apache:sde155f1667f3", "apache:scf804f4940d7"], "status": "REASONED"}, + "tls-version": {"text": "TLSv1.3 needs Apache 2.4.36+ with OpenSSL 1.1.1+; older builds use SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1.", "components": ["apache", "tls-min", "openssl"], "sources": ["apache:scf804f4940d7", "tls-min:scf804f4940d7", "openssl:scf804f4940d7"], "status": "REASONED"}, + "vhost-floor": {"text": "Independent per-vhost SSLProtocol needs Apache 2.4.42+, OpenSSL 1.1.1+ and client SNI; older builds also need the floor in global/base config.", "components": ["apache", "sni-min", "openssl"], "sources": ["apache:scf804f4940d7", "apache:s7c302c734a0d", "sni-min:scf804f4940d7", "openssl:scf804f4940d7"], "status": "REASONED"}, + "chain": {"text": "Apache 2.4.8+ permits the chain in SSLCertificateFile; SSLCertificateChainFile is deprecated.", "components": ["apache", "chain-min"], "sources": ["apache:scf804f4940d7", "chain-min:scf804f4940d7"], "status": "REASONED"}, + "hsts": {"text": "Header always sets HSTS after HTTPS works; includeSubDomains requires valid HTTPS everywhere.", "components": ["apache"], "sources": ["apache:sde155f1667f3", "apache:s1dbb2636d050"], "status": "REASONED"}, + "cipher-policy": {"text": "Generate explicit cipher policy with Mozilla rather than copying old lists.", "components": ["policy"], "sources": ["policy:s529b0eabe2ed"], "status": "REASONED"}, + "redirect": {"text": "The 80 vhost sends a permanent redirect to the canonical HTTPS host; retain only redirects and needed ACME HTTP-01 paths.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:sde155f1667f3"], "status": "REASONED"}, + "password-file": {"text": "Create a bcrypt htpasswd file with cost 12 and -c only for its first user; httpd 2.4.69 defaults bcrypt cost to 5 and OWASP recommends at least 10 for legacy bcrypt. Both password-file paths are guide choices.", "components": ["apache", "debian-packaging", "fedora-packaging", "htpasswd", "owasp"], "sources": ["apache:s99dcb49d86a4", "debian-packaging:s62f6ed2866ab", "debian-packaging:sd1c9de6b39aa", "fedora-packaging:s4479577a2e6a", "htpasswd:s309ecdd34379", "htpasswd:s7850e1e9dfa1", "htpasswd:s1c1a101a70e7", "owasp:s4531e65b6502"], "status": "REASONED"}, + "basic": {"text": "AuthType Basic, AuthName, AuthUserFile and Require valid-user protect the chosen site/path over TLS; enumerate every public path.", "components": ["apache"], "sources": ["apache:s99dcb49d86a4"], "status": "REASONED"}, + "mtls": {"text": "A separate machine-client 443 vhost requires a trusted certificate with SSLVerifyClient require and depth 2; SSLCACertificateFile is server/vhost scoped.", "components": ["apache"], "sources": ["apache:scf804f4940d7"], "status": "REASONED"}, + "mfa": {"text": "Basic is single-factor; Authelia v4.39.28 says Apache is unsupported for its proxy integration, and mod_auth_openidc v2.4.20.3 is an OIDC relying party. Provider MFA is guide advice; Cloudflare Access documents self-hosted application and MFA policy controls; fronting an Apache origin is this guide's suggested option and assessment. This guide assesses that an Apache origin reachable without passing through Access is not protected by it.", "components": ["apache", "authelia", "openidc", "cloudflare-access"], "sources": ["apache:s99dcb49d86a4", "authelia:s1de7fe72d011", "openidc:s01e183d16f03", "cloudflare-access:sfddb92e438ca", "cloudflare-access:s7cafe1b17afd", "cloudflare-access:sdcac732541da", "cloudflare-access:s603bdb6b24ab", "cloudflare-access:s863803bd9a1f"], "status": "REASONED"}, + "key-permissions": {"text": "Keep private keys root-owned and mode 0600.", "components": ["apache"], "sources": ["apache:sde155f1667f3"], "status": "REASONED"}, + "verify-config": {"text": "Run apachectl configtest before service reload; no outcome is recorded.", "components": ["apache"], "sources": ["apache:sde155f1667f3"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP should return 301 with an HTTPS Location.", "components": ["apache"], "sources": ["apache:sc07f97af99e3"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "Read printed status: curl can exit 0 for 200 or 401; expect 200 before auth and 401 after it.", "components": ["apache"], "sources": ["apache:s99dcb49d86a4"], "status": "REASONED", "verify": [1]}, + "verify-listeners": {"text": "Inspect the entire listener table, without filtering away unexpected ports.", "components": ["apache"], "sources": ["apache:sc07f97af99e3"], "status": "REASONED", "verify": [1]}, + "verify-unmatched": {"text": "Use an unconfigured certificate-covered URL name for both SNI and Host; 401 passes, 200 exposes a protected resource through another vhost. Host-only probes can misleadingly give 401 or 421.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:scf804f4940d7", "apache:s7c302c734a0d"], "status": "REASONED", "verify": [1]}, + "verify-vhost-review": {"text": "If no spare certificate-covered name exists, use apachectl -S and review that the first matching vhost cannot serve the protected DocumentRoot.", "components": ["apache"], "sources": ["apache:sc07f97af99e3", "apache:s7c302c734a0d"], "status": "REASONED", "verify": [2]} + } +} +```` +====================================================================== +==> nginx.md +====================================================================== + +# nginx: TLS and authentication + +Written against nginx source release-1.26.3, Certbot nginx configurator v2.3.0, Authelia nginx integration v4.39.28, oauth2-proxy nginx integration v7.15.4, Apache httpd source 2.4.69, Debian apache2 packaging 2.4.69-1, HSTS specification RFC 6797, and OWASP Password Storage Cheat Sheet commit 72865b1e; 3 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 28 claims, all reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + Get a certificate first: [free-certificates.md](free-certificates.md) for a public host (note that `certbot --nginx` edits the server block for you), or [self-signed.md](self-signed.md) for internal use. ## 1. HTTPS server block @@ -5516,57 +5990,233 @@ ss -tlnp # read every listener; 3000: the app itself: 127.0.0.1 only, never 0. - Debian apache2 packaging version 2.4.69-1 (checked October 2026): https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1 - OWASP Password Storage Cheat Sheet at commit 72865b1e (checked October 2026): https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13 -====================================================================== -==> lighttpd.md -====================================================================== - -# lighttpd: TLS and authentication +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/nginx.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + **Version basis** -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- tls: lighttpd TLS minimum (1.4.56) -- key: lighttpd separate-key minimum (1.4.53) -- redirect: lighttpd redirect default transition (1.4.75) -- docs: lighttpd documentation (unknown) -- modules-source: lighttpd module loader source (1.4.75) -- authn-source: lighttpd file authentication source (1.4.75) -- htpasswd-tool: Apache htpasswd manual (2.4) -- debian-package: Debian apache2-utils package (trixie) -- authelia: Authelia proxy support (v4.39.28) -- cloudflare-access: Cloudflare Access documentation (commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- fedora-package: Fedora httpd-tools package (unknown) -- lighttpd-verify-source: lighttpd verify source (1.4.75) -- systemd-reload: systemd reload manual (v262) -- apache-vhosts: Apache virtual-host source (2.4.69) -- release-news: lighttpd release NEWS (1.4.75) +- nginx-source: nginx source (release-1.26.3) +- ng: nginx documentation (unknown) +- certbot: Certbot nginx configurator (v2.3.0) +- authelia: Authelia nginx integration (v4.39.28) +- oauth2-proxy: oauth2-proxy nginx integration (v7.15.4) +- cloudflare-access: Cloudflare Access web applications (unknown) +- httpd: Apache httpd source (2.4.69) +- debian-apache2: Debian apache2 packaging (2.4.69-1) +- policy: Mozilla TLS policy (unknown) +- hsts: HSTS specification (RFC 6797) +- owasp: OWASP Password Storage Cheat Sheet (commit 72865b1e) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- tls-defaults (REASONED; tls, release-news): For this guide's 1.4.56-and-later scope, NEWS records the mod_openssl MinProtocol TLSv1.2 default in 1.4.56. Disabling SSLv2, SSLv3, TLS 1.0 and TLS 1.1 is this guide's reading of that floor; NEWS does not establish defaults for every later release. -- tls-listeners (REASONED; tls): Load mod_openssl and enable TLS on both :443 and [::]:443 with ssl.pemfile and ssl.privkey; omitting the IPv6 socket leaves IPv6 clients on plain HTTP. -- certificate-chain (REASONED; tls): ssl.pemfile uses fullchain.pem rather than a certificate without its chain. -- separate-key (REASONED; key, release-news): NEWS records optional mod_openssl ssl.privkey in 1.4.53. On older versions, concatenate certificate and key into ssl.pemfile (this guide's compatibility advice; the entry does not establish that procedure). -- protocol-floor (REASONED; tls, release-news): Set MinProtocol to TLSv1.2 with ssl.openssl.ssl-conf-cmd explicitly (this guide's advice); NEWS records that default for mod_openssl in 1.4.56, not every later release. -- module-defaults (REASONED; docs, modules-source): In lighttpd 1.4.75, mod_h2 is inserted unless server.feature-flags disables server.h2proto (enabled by default); when server.compat-module-load is enabled (the default), mod_indexfile and mod_dirlisting are conditionally added when enabled, mod_staticfile is added when absent, and compatibility handling can also add other modules, including authentication and TLS modules under their respective conditions. Load mod_redirect explicitly. -- http-redirect (REASONED; docs): For the http scheme, redirect to HTTPS preserving authority, path and query; port 80 must redirect rather than serve content. -- redirect-code (REASONED; redirect, release-news): NEWS records url.redirect-code = 308 as the new default in 1.4.75. Set 308 explicitly before 1.4.75 (this guide's advice; the entry does not specify each earlier default). -- basic-auth (REASONED; docs): Load mod_auth and mod_authn_file, select the htpasswd file backend and require valid-user Basic authentication on / over TLS; application login is preferable. -- password-file (REASONED; docs, authn-source, htpasswd-tool, debian-package, fedora-package): Create the user file with Apache htpasswd (Debian apache2-utils; Fedora httpd-tools is this guide's package assessment); lighttpd 1.4.75 reads username:stored-hash entries and checks {SHA} SHA-1 and $apr1$ internally, with other hashes of at least 13 characters checked through crypt() when built with crypt support; under USE_LIB_CRYPTO_MD4, the $1+ntlm$ path derives an NTLM hash and calls crypt() with that derived value and a $1$ salt. Check the build and platform's accepted hashes before choosing a flag; this check is this guide's compatibility advice. -- mfa (REASONED; docs, authelia, cloudflare-access): Basic authentication is single-factor; add MFA by fronting this service with Cloudflare Access or an MFA-capable proxy (this guide's requirement). Authelia v4.39.28's complete supported-proxy list has no lighttpd entry (this guide's inference from the table); Cloudflare Access documents self-hosted application and MFA policy controls; applying them to lighttpd is this guide's assessment. This guide assesses that a lighttpd origin reachable without passing through Access is not protected by it. -- verify-config (REASONED; docs, lighttpd-verify-source, systemd-reload): lighttpd 1.4.75 -tt -f /etc/lighttpd/lighttpd.conf tests the named file for syntax errors and initializes modules; configuration failures prevent the && reload (this guide's inference from the nonzero process result and shell chaining). The cited upstream unit starts from that file and defines a USR1 reload through ExecReload; systemctl reload triggers the service action, whose signal does not establish completion. Confirm the installed unit uses that file and defines reload (this guide's advice). -- verify-redirect (REASONED; docs): The HTTP header request should show a redirect to HTTPS; these are read-and-judge status checks. -- verify-auth (REASONED; docs): An HTTPS request without credentials should return 401 once authentication is enabled. -- verify-listeners (REASONED; tls): Read every listener with ss -tlnp: front-door authentication does not establish that no other listener serves content. The ss version is unrecorded. -- verify-host (REASONED; docs, lighttpd-verify-source, apache-vhosts): In lighttpd 1.4.75, request-time $HTTP["host"] matching uses the HTTP Host value, while TLS SNI can select mod_openssl settings before request headers; a Host outside a conditional auth.require falls back to global configuration for that rule (this guide's inference from conditional merging and mod_auth documentation). On a protected path, this guide interprets 401 as the pass and 200 as the protected path served without authentication on this TLS connection to a client that sends another host. Apache 2.4.69 uses the SNI hostname, when the client sends it and the server supports SNI, for name-based TLS virtual-host selection; varying Host while keeping URL and SNI fixed is this guide's auth-check advice. +- certbot (REASONED; ng, certbot): The guide says certbot --nginx edits the server block; certificate acquisition is delegated to the linked certificate guides. +- tls (REASONED; ng): 443 IPv4/IPv6 TLS uses certificate chain and private key, TLSv1.2/TLSv1.3 and ssl_prefer_server_ciphers off. +- http2 (REASONED; ng, nginx-source): At nginx release-1.26.3, http2 on is the HTTP/2 module directive; nginx 1.25.1 introduced it and deprecated the older listen 443 ssl http2 parameter. +- tokens (REASONED; ng): server_tokens off suppresses nginx version in headers and error pages. +- hsts (REASONED; ng, hsts, nginx-source): Send HSTS only after HTTPS works; includeSubDomains commits every subdomain for max-age. +- hsts-inheritance (REASONED; ng, nginx-source): At nginx release-1.26.3, a location add_header replaces inherited server headers; always changes response-code coverage, not inheritance. +- proxy (REASONED; ng): Proxy to loopback 127.0.0.1:3000 and set Host, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host. +- forwarded-host (REASONED; ng): Overwrite X-Forwarded-Host: client-set values otherwise pass upstream. +- cipher-policy (REASONED; ng, policy): Use Mozilla for explicit cipher policy; retain the stated protocol floor. +- tickets (REASONED; ng, nginx-source): At nginx release-1.26.3, ssl_session_tickets defaults on; nginx 1.23.2 introduced automatic ticket-key rotation with a shared ssl_session_cache. +- redirect (REASONED; ng): Port 80 on IPv4/IPv6 redirects with 301; a default server reflecting $host can redirect unmatched hosts off-site, so use 444 or a canonical host. +- password-file (REASONED; ng, httpd, owasp, debian-apache2): At httpd 2.4.69, htpasswd -B selects bcrypt, bare -B uses cost 5, and -C sets the cost; the OWASP cheat sheet at commit 72865b1e recommends at least 10 for legacy bcrypt. Debian apache2 2.4.69-1 packages htpasswd in apache2-utils. +- basic (REASONED; ng): auth_basic and auth_basic_user_file gate the existing proxied location over TLS; preserve its forwarded headers. +- mtls (REASONED; ng, nginx-source): At nginx release-1.26.3, ssl_client_certificate and ssl_verify_client on configure trusted client-certificate verification. +- mfa (REASONED; ng, nginx-source, authelia, oauth2-proxy, cloudflare-access): At nginx release-1.26.3, auth_request permits 2xx authentication subresponses and returns 401/403; Authelia v4.39.28 and oauth2-proxy v7.15.4 document nginx auth_request integration, and Cloudflare Access documents protecting self-hosted web applications; the nginx source cited here does not cover those integrations. MFA is this guide's requirement; whether a given installation enforces it depends on its configured portal or Access policy (this guide's assessment). +- body-limit (REASONED; ng): client_max_body_size defaults to 1m; 10m rejects larger bodies with 413, and 0 disables the check. +- rate-limit (REASONED; ng): Per-address limit_req uses a 10r/s zone, burst 20 and nodelay; rejection status defaults to 503. +- connection-limit (REASONED; ng): limit_conn apiconn 10 caps concurrent connections per client address; it can also return 503. +- location-auth (REASONED; ng): Keep limits in the authenticated location; sibling locations do not inherit its auth or proxy headers. Repeat auth or inherit it from server; auth_basic off is deliberate. +- read-timeout (REASONED; ng): proxy_read_timeout 60s limits gaps between upstream reads, not total response duration; active streams can continue indefinitely. +- client-address (REASONED; ng): At a direct edge trust the appended last X-Forwarded-For entry; behind a CDN, recover the client with trusted set_real_ip_from and real_ip_header before per-client limits. +- verify-config (REASONED; ng): Run nginx -t before reload; no configuration-test outcome is recorded. +- verify-redirect (REASONED; ng): HTTP should return 301 with an HTTPS Location. +- verify-tls (REASONED; ng): HTTPS must validate without -k; TLS 1.1-only rejection must be protocol_version, not a generic/local failure; test TLS 1.0 separately. +- verify-auth (REASONED; ng): An open path may return 200; the protected path must reject no credentials with 401/403, and valid credentials reach the app rather than 401. +- verify-size (REASONED; ng): Authenticated 9M succeeds normally only after raising the 1m default; 11M returns 413. Disable the check with 0 in isolation to distinguish backend refusal. +- verify-rate (REASONED; ng): Concurrent unauthenticated probes exercise pre-auth limits; 503 does not identify the limiter. Compare both off and each alone with measured arrival rate. +- verify-backend (REASONED; ng): Inspect every listener: direct public access to backend 3000 bypasses proxy TLS/auth even when front-door probes pass. + +````json +{ + "schema": 1, + "checked": "2026-10-17", + "documentation_checked": "2026-09", + "body_sha256": "07eda7c75b5cada2d56a730e2c7a33df34730eec5139f55d0545aad278723811", + "components": { + "nginx-source": { + "name": "nginx source", + "basis": "release-1.26.3", + "sources": { + "s0ae4e9bc8d07": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L216-L218", + "s002242c2d0cd": "https://github.com/nginx/nginx/blob/release-1.26.3/docs/xml/nginx/changes.xml#L470-L480", + "s305b7194043c": "https://github.com/nginx/nginx/blob/release-1.26.3/docs/xml/nginx/changes.xml#L739-L775", + "sbea253fd242b": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/v2/ngx_http_v2_module.c#L76-L81", + "s936193a42402": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/ngx_http_core_module.c#L4181-L4188", + "sac3cbbf7fc48": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L857-L862", + "s66c86979411e": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L153-L172", + "sabb195c173d5": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L788-L802", + "sa930199b563a": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_ssl_module.c#L74-L80", + "seb1f8a9d91f9": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L666-L687", + "sf6eac8f6749d": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L788-L796", + "s3df6e64cd6a3": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_request_module.c#L135-L182", + "sced570e33c0f": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/ngx_http_request.h#L78-L138", + "sfafe27857e33": "https://github.com/nginx/nginx/blob/release-1.26.3/src/event/ngx_event_openssl.c#L4568-L4576" + } + }, + "ng": { + "name": "nginx documentation", + "basis": "unknown", + "sources": { + "s6a0728ca7609": "https://nginx.org/en/docs/http/configuring_https_servers.html", + "s40bdf1af1596": "https://nginx.org/en/docs/http/ngx_http_core_module.html", + "sb3bee9429629": "https://nginx.org/en/docs/http/ngx_http_limit_req_module.html", + "sddc43aac8779": "https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html", + "sf3430c5a0b22": "https://nginx.org/en/docs/http/ngx_http_proxy_module.html", + "sf32ce2956917": "https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html", + "sddacc1191f5f": "https://nginx.org/en/docs/http/ngx_http_realip_module.html" + } + }, + "certbot": { + "name": "Certbot nginx configurator", + "basis": "v2.3.0", + "sources": { + "s3a02bfe1d7ad": "https://github.com/certbot/certbot/blob/v2.3.0/certbot-nginx/certbot_nginx/_internal/configurator.py#L711" + } + }, + "authelia": { + "name": "Authelia nginx integration", + "basis": "v4.39.28", + "sources": { + "s279cd6449b53": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L39-L45", + "sdf01968ab996": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L245-L247", + "s237ae14bbb4a": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L517-L519" + } + }, + "oauth2-proxy": { + "name": "oauth2-proxy nginx integration", + "basis": "v7.15.4", + "sources": { + "sc62400f0ea8a": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/integrations/nginx.md#L16-L18" + } + }, + "cloudflare-access": { + "name": "Cloudflare Access web applications", + "basis": "unknown", + "sources": { + "sfe56578bedf4": "https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/" + } + }, + "httpd": { + "name": "Apache httpd source", + "basis": "2.4.69", + "sources": { + "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128", + "sf711558bdf3c": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L45", + "s1c1a101a70e7": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L290-L295", + "s4482c55d79a2": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L382" + } + }, + "debian-apache2": { + "name": "Debian apache2 packaging", + "basis": "2.4.69-1", + "sources": { + "sc2a26b3a7af5": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/control#L92-101", + "s62f6ed2866ab": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1" + } + }, + "policy": { + "name": "Mozilla TLS policy", + "basis": "unknown", + "sources": { + "s529b0eabe2ed": "https://ssl-config.mozilla.org/" + } + }, + "hsts": { + "name": "HSTS specification", + "basis": "RFC 6797", + "sources": { + "s3f5af5f440f9": "https://www.rfc-editor.org/rfc/rfc6797.html#section-6.1.2" + } + }, + "owasp": { + "name": "OWASP Password Storage Cheat Sheet", + "basis": "commit 72865b1e", + "sources": { + "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" + } + } + }, + "claims": { + "certbot": {"text": "The guide says certbot --nginx edits the server block; certificate acquisition is delegated to the linked certificate guides.", "components": ["ng", "certbot"], "sources": ["ng:s6a0728ca7609", "certbot:s3a02bfe1d7ad"], "status": "REASONED"}, + "tls": {"text": "443 IPv4/IPv6 TLS uses certificate chain and private key, TLSv1.2/TLSv1.3 and ssl_prefer_server_ciphers off.", "components": ["ng"], "sources": ["ng:s6a0728ca7609"], "status": "REASONED"}, + "http2": {"text": "At nginx release-1.26.3, http2 on is the HTTP/2 module directive; nginx 1.25.1 introduced it and deprecated the older listen 443 ssl http2 parameter.", "components": ["ng", "nginx-source"], "sources": ["ng:s40bdf1af1596", "nginx-source:s002242c2d0cd", "nginx-source:sbea253fd242b", "nginx-source:s936193a42402"], "status": "REASONED"}, + "tokens": {"text": "server_tokens off suppresses nginx version in headers and error pages.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, + "hsts": {"text": "Send HSTS only after HTTPS works; includeSubDomains commits every subdomain for max-age.", "components": ["ng", "hsts", "nginx-source"], "sources": ["ng:s6a0728ca7609", "hsts:s3f5af5f440f9", "nginx-source:s0ae4e9bc8d07"], "status": "REASONED"}, + "hsts-inheritance": {"text": "At nginx release-1.26.3, a location add_header replaces inherited server headers; always changes response-code coverage, not inheritance.", "components": ["ng", "nginx-source"], "sources": ["ng:s40bdf1af1596", "nginx-source:s0ae4e9bc8d07", "nginx-source:seb1f8a9d91f9", "nginx-source:sf6eac8f6749d"], "status": "REASONED"}, + "proxy": {"text": "Proxy to loopback 127.0.0.1:3000 and set Host, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, + "forwarded-host": {"text": "Overwrite X-Forwarded-Host: client-set values otherwise pass upstream.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, + "cipher-policy": {"text": "Use Mozilla for explicit cipher policy; retain the stated protocol floor.", "components": ["ng", "policy"], "sources": ["ng:s6a0728ca7609", "policy:s529b0eabe2ed"], "status": "REASONED"}, + "tickets": {"text": "At nginx release-1.26.3, ssl_session_tickets defaults on; nginx 1.23.2 introduced automatic ticket-key rotation with a shared ssl_session_cache.", "components": ["ng", "nginx-source"], "sources": ["ng:s6a0728ca7609", "nginx-source:sac3cbbf7fc48", "nginx-source:s305b7194043c", "nginx-source:sfafe27857e33"], "status": "REASONED"}, + "redirect": {"text": "Port 80 on IPv4/IPv6 redirects with 301; a default server reflecting $host can redirect unmatched hosts off-site, so use 444 or a canonical host.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, + "password-file": {"text": "At httpd 2.4.69, htpasswd -B selects bcrypt, bare -B uses cost 5, and -C sets the cost; the OWASP cheat sheet at commit 72865b1e recommends at least 10 for legacy bcrypt. Debian apache2 2.4.69-1 packages htpasswd in apache2-utils.", "components": ["ng", "httpd", "owasp", "debian-apache2"], "sources": ["ng:sf32ce2956917", "debian-apache2:sc2a26b3a7af5", "debian-apache2:s62f6ed2866ab", "httpd:s309ecdd34379", "httpd:sf711558bdf3c", "httpd:s1c1a101a70e7", "httpd:s4482c55d79a2", "owasp:s4531e65b6502"], "status": "REASONED"}, + "basic": {"text": "auth_basic and auth_basic_user_file gate the existing proxied location over TLS; preserve its forwarded headers.", "components": ["ng"], "sources": ["ng:sf32ce2956917", "ng:sf3430c5a0b22"], "status": "REASONED"}, + "mtls": {"text": "At nginx release-1.26.3, ssl_client_certificate and ssl_verify_client on configure trusted client-certificate verification.", "components": ["ng", "nginx-source"], "sources": ["ng:s6a0728ca7609", "nginx-source:s66c86979411e", "nginx-source:sabb195c173d5", "nginx-source:sa930199b563a"], "status": "REASONED"}, + "mfa": {"text": "At nginx release-1.26.3, auth_request permits 2xx authentication subresponses and returns 401/403; Authelia v4.39.28 and oauth2-proxy v7.15.4 document nginx auth_request integration, and Cloudflare Access documents protecting self-hosted web applications; the nginx source cited here does not cover those integrations. MFA is this guide's requirement; whether a given installation enforces it depends on its configured portal or Access policy (this guide's assessment).", "components": ["ng", "nginx-source", "authelia", "oauth2-proxy", "cloudflare-access"], "sources": ["ng:sf32ce2956917", "nginx-source:s3df6e64cd6a3", "nginx-source:sced570e33c0f", "authelia:s279cd6449b53", "authelia:sdf01968ab996", "authelia:s237ae14bbb4a", "oauth2-proxy:sc62400f0ea8a", "cloudflare-access:sfe56578bedf4"], "status": "REASONED"}, + "body-limit": {"text": "client_max_body_size defaults to 1m; 10m rejects larger bodies with 413, and 0 disables the check.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED"}, + "rate-limit": {"text": "Per-address limit_req uses a 10r/s zone, burst 20 and nodelay; rejection status defaults to 503.", "components": ["ng"], "sources": ["ng:sb3bee9429629"], "status": "REASONED"}, + "connection-limit": {"text": "limit_conn apiconn 10 caps concurrent connections per client address; it can also return 503.", "components": ["ng"], "sources": ["ng:sddc43aac8779"], "status": "REASONED"}, + "location-auth": {"text": "Keep limits in the authenticated location; sibling locations do not inherit its auth or proxy headers. Repeat auth or inherit it from server; auth_basic off is deliberate.", "components": ["ng"], "sources": ["ng:sf32ce2956917", "ng:s40bdf1af1596", "ng:sf3430c5a0b22"], "status": "REASONED"}, + "read-timeout": {"text": "proxy_read_timeout 60s limits gaps between upstream reads, not total response duration; active streams can continue indefinitely.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED"}, + "client-address": {"text": "At a direct edge trust the appended last X-Forwarded-For entry; behind a CDN, recover the client with trusted set_real_ip_from and real_ip_header before per-client limits.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22", "ng:sddacc1191f5f", "ng:sb3bee9429629", "ng:sddc43aac8779"], "status": "REASONED"}, + "verify-config": {"text": "Run nginx -t before reload; no configuration-test outcome is recorded.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP should return 301 with an HTTPS Location.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, + "verify-tls": {"text": "HTTPS must validate without -k; TLS 1.1-only rejection must be protocol_version, not a generic/local failure; test TLS 1.0 separately.", "components": ["ng"], "sources": ["ng:s6a0728ca7609"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "An open path may return 200; the protected path must reject no credentials with 401/403, and valid credentials reach the app rather than 401.", "components": ["ng"], "sources": ["ng:sf32ce2956917"], "status": "REASONED", "verify": [1]}, + "verify-size": {"text": "Authenticated 9M succeeds normally only after raising the 1m default; 11M returns 413. Disable the check with 0 in isolation to distinguish backend refusal.", "components": ["ng"], "sources": ["ng:s40bdf1af1596"], "status": "REASONED", "verify": [1]}, + "verify-rate": {"text": "Concurrent unauthenticated probes exercise pre-auth limits; 503 does not identify the limiter. Compare both off and each alone with measured arrival rate.", "components": ["ng"], "sources": ["ng:sb3bee9429629", "ng:sddc43aac8779", "ng:sf32ce2956917"], "status": "REASONED", "verify": [1]}, + "verify-backend": {"text": "Inspect every listener: direct public access to backend 3000 bypasses proxy TLS/auth even when front-door probes pass.", "components": ["ng"], "sources": ["ng:sf3430c5a0b22"], "status": "REASONED", "verify": [1]} + } +} +```` +====================================================================== +==> lighttpd.md +====================================================================== + +# lighttpd: TLS and authentication + +Written against lighttpd TLS minimum 1.4.56, lighttpd separate-key minimum 1.4.53, lighttpd redirect default transition 1.4.75, lighttpd module loader source 1.4.75, lighttpd file authentication source 1.4.75, lighttpd verify source 1.4.75, Apache htpasswd manual 2.4, and Debian apache2-utils package trixie, plus 5 more recorded bases; 2 further sources carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 16 claims, all reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + Applies to lighttpd 1.4.56 and later. NEWS records the `mod_openssl` `MinProtocol TLSv1.2` default in its 1.4.56 release section (https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1660-L1660; https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1927-L1927). Disabling SSLv2/SSLv3/TLS 1.0/TLS 1.1 is this guide's reading of that floor; NEWS does not establish defaults for every later release. Get a certificate first: [free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md). ## 1. Enable TLS @@ -5720,68 +6370,247 @@ curl -q -s -o /dev/null -w '%{http_code}\n' -H 'Host: not-configured.example' \ - Cloudflare Access MFA policy controls at commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104 (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81 - Fedora `httpd-tools` package (checked October 2026): https://packages.fedoraproject.org/pkgs/httpd/httpd-tools/ -====================================================================== -==> caddy.md -====================================================================== - -# Caddy: TLS and authentication +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/lighttpd.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + **Version basis** -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. -Metadata reviewed 2026-09-27; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- admin: Caddy admin source (v2.11.4) -- tags: Official image tag map (d82ca5102fa6735be29d5e1fc6ce03af77eb091e) -- images: Caddy Docker source (fba2853501d36e8a72f946ac8cb7ff64d07e48f2) -- dist: Default Caddyfile source (33ae08ff08d168572df2956ed14fbc4949880d94) -- docker-network: Docker networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- docs-rolling: Caddy documentation (rolling) (unknown) -- letsencrypt-notice: Let's Encrypt expiration-email notice (2025) -- service: caddyserver/dist service unit (33ae08ff08d168572df2956ed14fbc4949880d94) -- systemd: systemd manual (v262) -- cloudflare: Cloudflare docs (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- ratelimit-source: caddy-ratelimit source (5625512f24f6f59d6f64fb3aafe5eecff0b286db) +- tls: lighttpd TLS minimum (1.4.56) +- key: lighttpd separate-key minimum (1.4.53) +- redirect: lighttpd redirect default transition (1.4.75) +- modules-source: lighttpd module loader source (1.4.75) +- authn-source: lighttpd file authentication source (1.4.75) +- lighttpd-verify-source: lighttpd verify source (1.4.75) +- docs: lighttpd documentation (unknown) +- htpasswd-tool: Apache htpasswd manual (2.4) +- debian-package: Debian apache2-utils package (trixie) +- authelia: Authelia proxy support (v4.39.28) +- cloudflare-access: Cloudflare Access documentation (commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- fedora-package: Fedora httpd-tools package (unknown) +- apache-vhosts: Apache virtual-host source (2.4.69) +- systemd-reload: systemd reload manual (v262) +- release-news: lighttpd release NEWS (1.4.75) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- auto-https (REASONED; docs-rolling): Caddy 2 obtains and renews public certificates and redirects HTTP to HTTPS; DNS must point here and the example requires public 80/443. -- issuers (REASONED; docs-rolling): Public certificates come from Let's Encrypt or ZeroSSL and renew automatically. -- renewal-monitoring (REASONED; docs-rolling, letsencrypt-notice): Set the ACME email contact but monitor renewal yourself: Let's Encrypt stopped expiration emails. -- backend (REASONED; docs-rolling): reverse_proxy sends traffic to 127.0.0.1:3000; keep the app private to prevent TLS/auth bypass. -- hsts (REASONED; docs-rolling): Caddy sends no HSTS automatically; add it only after HTTPS works across every included subdomain. -- forwarded (REASONED; docs-rolling): Caddy ignores untrusted client X-Forwarded-* by default; configure trusted_proxies behind a trusted CDN/proxy. -- internal-tls (REASONED; docs-rolling): tls internal uses a local CA; clients must trust its root, with caddy trust for the local host. tls also accepts explicit cert/key files. -- basic (REASONED; docs-rolling): basic_auth uses hashed passwords from caddy hash-password; it was named basicauth before v2.8.0 and remains single-factor. -- path-auth (REASONED; docs-rolling): Protect /admin and /admin/* together: path matches are exact and multiple paths are OR'ed. -- mfa (REASONED; docs-rolling, cloudflare): forward_auth (Caddy 2.5+) can delegate human authentication to Authelia; Cloudflare Access is a proposed layer that enforces MFA when its governing Access setting (policy over application over organization) requires it. -- access-origin (REASONED; docs-rolling, cloudflare): Cloudflare Access can require MFA when configured; the guide reasons that direct origin ingress can bypass Access, so restrict it; validate the Access JWT at the origin, which a tunnel's `access` setting has `cloudflared` do. Caddy still answers directly on 443. -- body-limit (REASONED; docs-rolling): Add request_body max_size 10MB inside the existing authenticated site; replacing the site without basic_auth makes it public. -- rate-limit (REASONED; docs-rolling, ratelimit-source): The standard build has no rate_limit directive; use a compiled community caddy-ratelimit module or an upstream layer. -- admin-bind (REASONED; admin, docs-rolling): The admin API defaults to localhost:2019 as of v2.11.4; never publish it or reverse-proxy to it. -- admin-authority (REASONED; docs-rolling): Reachable unauthenticated admin clients can replace config at POST /load, edit /config/ or stop with POST /stop. -- admin-isolation (REASONED; docs-rolling): Host/Origin checks are not process isolation; local untrusted workloads require a permissioned Unix socket. -- docker-admin (REASONED; tags, images, dist, admin): The official 2.11.4 Alpine, Windows Server Core 2022/2025, and Nano Server 2022/2025 runtime tags map to fba28535 Dockerfiles that EXPOSE 2019 and run the default Caddyfile without admin; absent overrides, the endpoint stays namespace-local. -- admin-override (REASONED; admin, dist, docs-rolling): A mounted admin setting or CADDY_ADMIN changes the address; do not set 0.0.0.0:2019 to make publication work. -- namespace (REASONED; images, dist, admin, docker-network, docs-rolling): The shipped admin default is localhost:2019. On Docker Engine on Linux, bridge networking keeps loopback container-local; host or container networking shares a namespace, so treat its processes as potential admin clients. -- admin-socket (REASONED; docs-rolling): admin unix//run/caddy/admin.sock uses owner-only mode 0200 by default; append |0600 to set it explicitly. -- runtime-directory (REASONED; docs-rolling, service, systemd): The dist service at 33ae08ff runs as caddy without RuntimeDirectory; systemd v262 creates /run/caddy for RuntimeDirectory=caddy at service start and owns it as caddy. Restart for the socket switch is guide reasoning. -- reload (REASONED; docs-rolling, service): The dist service at 33ae08ff uses caddy reload --config /etc/caddy/Caddyfile --force; API reload uses the Caddyfile admin address. With admin off, guide reasoning requires systemd restart. -- signal-reload (REASONED; docs-rolling): A manual run without --resume may reload startup config via SIGUSR1; API changes or reload with a different file/adapter disable signal reloads. -- on-demand (REASONED; docs-rolling): Do not enable unrestricted on_demand TLS without an ask endpoint or host allowlist; this guide does not enable it. -- verify-config (REASONED; docs-rolling): Validate /etc/caddy/Caddyfile; no validation outcome is recorded. -- verify-redirect (REASONED; docs-rolling): HTTP should redirect to HTTPS. -- verify-auth (REASONED; docs-rolling): With site-wide auth, HTTPS without credentials returns 401. -- verify-scope (REASONED; docs-rolling): The @admin variant must return 401 for /admin and /admin/x, but an app response rather than 401 for a non-admin path. -- verify-size (REASONED; docs-rolling): Authenticated 1M reaches the app and 11M returns 413; 401/000 void the control. Auth can prevent body reads; remove request_body in isolation to attribute refusal. -- verify-backend (REASONED; docs-rolling): Read all listeners: app 3000 must be loopback-only even if proxy checks pass. -- verify-admin (REASONED; docs-rolling): TCP admin must bind only loopback; absent 2019 requires confirming admin off or a socket. Check Unix socket existence and owner restrictions separately. +- tls-defaults (REASONED; tls, release-news): For this guide's 1.4.56-and-later scope, NEWS records the mod_openssl MinProtocol TLSv1.2 default in 1.4.56. Disabling SSLv2, SSLv3, TLS 1.0 and TLS 1.1 is this guide's reading of that floor; NEWS does not establish defaults for every later release. +- tls-listeners (REASONED; tls): Load mod_openssl and enable TLS on both :443 and [::]:443 with ssl.pemfile and ssl.privkey; omitting the IPv6 socket leaves IPv6 clients on plain HTTP. +- certificate-chain (REASONED; tls): ssl.pemfile uses fullchain.pem rather than a certificate without its chain. +- separate-key (REASONED; key, release-news): NEWS records optional mod_openssl ssl.privkey in 1.4.53. On older versions, concatenate certificate and key into ssl.pemfile (this guide's compatibility advice; the entry does not establish that procedure). +- protocol-floor (REASONED; tls, release-news): Set MinProtocol to TLSv1.2 with ssl.openssl.ssl-conf-cmd explicitly (this guide's advice); NEWS records that default for mod_openssl in 1.4.56, not every later release. +- module-defaults (REASONED; docs, modules-source): In lighttpd 1.4.75, mod_h2 is inserted unless server.feature-flags disables server.h2proto (enabled by default); when server.compat-module-load is enabled (the default), mod_indexfile and mod_dirlisting are conditionally added when enabled, mod_staticfile is added when absent, and compatibility handling can also add other modules, including authentication and TLS modules under their respective conditions. Load mod_redirect explicitly. +- http-redirect (REASONED; docs): For the http scheme, redirect to HTTPS preserving authority, path and query; port 80 must redirect rather than serve content. +- redirect-code (REASONED; redirect, release-news): NEWS records url.redirect-code = 308 as the new default in 1.4.75. Set 308 explicitly before 1.4.75 (this guide's advice; the entry does not specify each earlier default). +- basic-auth (REASONED; docs): Load mod_auth and mod_authn_file, select the htpasswd file backend and require valid-user Basic authentication on / over TLS; application login is preferable. +- password-file (REASONED; docs, authn-source, htpasswd-tool, debian-package, fedora-package): Create the user file with Apache htpasswd (Debian apache2-utils; Fedora httpd-tools is this guide's package assessment); lighttpd 1.4.75 reads username:stored-hash entries and checks {SHA} SHA-1 and $apr1$ internally, with other hashes of at least 13 characters checked through crypt() when built with crypt support; under USE_LIB_CRYPTO_MD4, the $1+ntlm$ path derives an NTLM hash and calls crypt() with that derived value and a $1$ salt. Check the build and platform's accepted hashes before choosing a flag; this check is this guide's compatibility advice. +- mfa (REASONED; docs, authelia, cloudflare-access): Basic authentication is single-factor; add MFA by fronting this service with Cloudflare Access or an MFA-capable proxy (this guide's requirement). Authelia v4.39.28's complete supported-proxy list has no lighttpd entry (this guide's inference from the table); Cloudflare Access documents self-hosted application and MFA policy controls; applying them to lighttpd is this guide's assessment. This guide assesses that a lighttpd origin reachable without passing through Access is not protected by it. +- verify-config (REASONED; docs, lighttpd-verify-source, systemd-reload): lighttpd 1.4.75 -tt -f /etc/lighttpd/lighttpd.conf tests the named file for syntax errors and initializes modules; configuration failures prevent the && reload (this guide's inference from the nonzero process result and shell chaining). The cited upstream unit starts from that file and defines a USR1 reload through ExecReload; systemctl reload triggers the service action, whose signal does not establish completion. Confirm the installed unit uses that file and defines reload (this guide's advice). +- verify-redirect (REASONED; docs): The HTTP header request should show a redirect to HTTPS; these are read-and-judge status checks. +- verify-auth (REASONED; docs): An HTTPS request without credentials should return 401 once authentication is enabled. +- verify-listeners (REASONED; tls): Read every listener with ss -tlnp: front-door authentication does not establish that no other listener serves content. The ss version is unrecorded. +- verify-host (REASONED; docs, lighttpd-verify-source, apache-vhosts): In lighttpd 1.4.75, request-time $HTTP["host"] matching uses the HTTP Host value, while TLS SNI can select mod_openssl settings before request headers; a Host outside a conditional auth.require falls back to global configuration for that rule (this guide's inference from conditional merging and mod_auth documentation). On a protected path, this guide interprets 401 as the pass and 200 as the protected path served without authentication on this TLS connection to a client that sends another host. Apache 2.4.69 uses the SNI hostname, when the client sends it and the server supports SNI, for name-based TLS virtual-host selection; varying Host while keeping URL and SNI fixed is this guide's auth-check advice. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "aa010c3e401699d0b9ff9541720750e57529d097494238fca31e5db6a0c3826f", + "components": { + "tls": { + "name": "lighttpd TLS minimum", + "basis": "1.4.56", + "sources": { + "sbff3cb2e01a8": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_SSL" + } + }, + "key": { + "name": "lighttpd separate-key minimum", + "basis": "1.4.53", + "sources": { + "sbff3cb2e01a8": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_SSL" + } + }, + "redirect": { + "name": "lighttpd redirect default transition", + "basis": "1.4.75", + "sources": { + "s6de629323c64": "https://redmine.lighttpd.net/projects/lighttpd/wiki/HowToRedirectHttpToHttps" + } + }, + "modules-source": { + "name": "lighttpd module loader source", + "basis": "1.4.75", + "sources": { + "s8680e561bed2": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L395-L544", + "s8e9688979427": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L270-L290", + "s9bd716d27e8d": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L382-L393", + "s21288573041a": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L662-L945", + "s67bb3733d81c": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L1684-L1711" + } + }, + "authn-source": { + "name": "lighttpd file authentication source", + "basis": "1.4.75", + "sources": { + "s2c11c0fbc0ca": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_authn_file.c#L340-L397", + "se971effb489f": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_authn_file.c#L613-L740" + } + }, + "lighttpd-verify-source": { + "name": "lighttpd verify source", + "basis": "1.4.75", + "sources": { + "s0bbb4c861d5e": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/lighttpd.8#L22-L23", + "sca076bcd18dc": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/lighttpd.8#L33-L37", + "s9e69235fbbc7": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1439-L1451", + "s87104d328493": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1466-L1467", + "sba72bab46867": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1510-L1523", + "s2faa9b3149ac": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L1942-L1953", + "s87b4b266bb0e": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L2320-L2384", + "sa989ef425f1d": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/server.c#L229-L235", + "sf9d56b052c61": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/doc/systemd/lighttpd.service#L5-L11", + "s6a29b290d83a": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/request.c#L423-L430", + "s2fc4cf82abd9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/request.c#L1235-L1247", + "s3f692a48bd01": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile-glue.c#L504-L508", + "sbb09703388c3": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/configfile.c#L213-L223", + "s8bb6bd7774f9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_openssl.c#L1025-L1034", + "sb4c43fa1a807": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/mod_openssl.c#L1294-L1330", + "s9e49b928cc81": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/src/response.c#L227-L228" + } + }, + "docs": { + "name": "lighttpd documentation", + "basis": "unknown", + "sources": { + "s1dbd1ce27da5": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Mod_auth", + "s6d5666b862a0": "https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ConfigurationOptions", + "s6de629323c64": "https://redmine.lighttpd.net/projects/lighttpd/wiki/HowToRedirectHttpToHttps" + } + }, + "htpasswd-tool": { + "name": "Apache htpasswd manual", + "basis": "2.4", + "sources": { + "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" + } + }, + "debian-package": { + "name": "Debian apache2-utils package", + "basis": "trixie", + "sources": { + "s436bdf5f1fb9": "https://packages.debian.org/trixie/apache2-utils" + } + }, + "authelia": { + "name": "Authelia proxy support", + "basis": "v4.39.28", + "sources": { + "s77083bc8faa2": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L24-L36" + } + }, + "cloudflare-access": { + "name": "Cloudflare Access documentation", + "basis": "commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "sfddb92e438ca": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L14-L16", + "s7cafe1b17afd": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54", + "s603bdb6b24ab": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L45-L45", + "s863803bd9a1f": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L48-L48", + "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" + } + }, + "fedora-package": { + "name": "Fedora httpd-tools package", + "basis": "unknown", + "sources": { + "s919088d03a5e": "https://packages.fedoraproject.org/pkgs/httpd/httpd-tools/" + } + }, + "apache-vhosts": { + "name": "Apache virtual-host source", + "basis": "2.4.69", + "sources": { + "s558db45d4d61": "https://github.com/apache/httpd/blob/2.4.69/docs/manual/vhosts/details.xml#L217-L231" + } + }, + "systemd-reload": { + "name": "systemd reload manual", + "basis": "v262", + "sources": { + "sb3f5069cd250": "https://github.com/systemd/systemd/blob/v262/man/systemctl.xml#L462-L474", + "s441757b7048f": "https://github.com/systemd/systemd/blob/v262/man/systemd.service.xml#L498-L519" + } + }, + "release-news": { + "name": "lighttpd release NEWS", + "basis": "1.4.75", + "sources": { + "s4e990a9e5ec1": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1660-L1660", + "se97c93bf64a8": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L1927-L1927", + "s1658d22c71f9": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L2465-L2465", + "s95dd525fe4ac": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L2505-L2505", + "sc0d0162991d5": "https://git.lighttpd.net/lighttpd/lighttpd1.4/src/tag/lighttpd-1.4.75/NEWS#L6-L7" + } + } + }, + "claims": { + "tls-defaults": {"text": "For this guide's 1.4.56-and-later scope, NEWS records the mod_openssl MinProtocol TLSv1.2 default in 1.4.56. Disabling SSLv2, SSLv3, TLS 1.0 and TLS 1.1 is this guide's reading of that floor; NEWS does not establish defaults for every later release.", "components": ["tls", "release-news"], "sources": ["tls:sbff3cb2e01a8", "release-news:s4e990a9e5ec1", "release-news:se97c93bf64a8"], "status": "REASONED"}, + "tls-listeners": {"text": "Load mod_openssl and enable TLS on both :443 and [::]:443 with ssl.pemfile and ssl.privkey; omitting the IPv6 socket leaves IPv6 clients on plain HTTP.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED"}, + "certificate-chain": {"text": "ssl.pemfile uses fullchain.pem rather than a certificate without its chain.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED"}, + "separate-key": {"text": "NEWS records optional mod_openssl ssl.privkey in 1.4.53. On older versions, concatenate certificate and key into ssl.pemfile (this guide's compatibility advice; the entry does not establish that procedure).", "components": ["key", "release-news"], "sources": ["key:sbff3cb2e01a8", "release-news:s1658d22c71f9", "release-news:s95dd525fe4ac"], "status": "REASONED"}, + "protocol-floor": {"text": "Set MinProtocol to TLSv1.2 with ssl.openssl.ssl-conf-cmd explicitly (this guide's advice); NEWS records that default for mod_openssl in 1.4.56, not every later release.", "components": ["tls", "release-news"], "sources": ["tls:sbff3cb2e01a8", "release-news:s4e990a9e5ec1", "release-news:se97c93bf64a8"], "status": "REASONED"}, + "module-defaults": {"text": "In lighttpd 1.4.75, mod_h2 is inserted unless server.feature-flags disables server.h2proto (enabled by default); when server.compat-module-load is enabled (the default), mod_indexfile and mod_dirlisting are conditionally added when enabled, mod_staticfile is added when absent, and compatibility handling can also add other modules, including authentication and TLS modules under their respective conditions. Load mod_redirect explicitly.", "components": ["docs", "modules-source"], "sources": ["docs:s6d5666b862a0", "modules-source:s8680e561bed2", "modules-source:s8e9688979427", "modules-source:s9bd716d27e8d", "modules-source:s21288573041a", "modules-source:s67bb3733d81c"], "status": "REASONED"}, + "http-redirect": {"text": "For the http scheme, redirect to HTTPS preserving authority, path and query; port 80 must redirect rather than serve content.", "components": ["docs"], "sources": ["docs:s6de629323c64"], "status": "REASONED"}, + "redirect-code": {"text": "NEWS records url.redirect-code = 308 as the new default in 1.4.75. Set 308 explicitly before 1.4.75 (this guide's advice; the entry does not specify each earlier default).", "components": ["redirect", "release-news"], "sources": ["redirect:s6de629323c64", "release-news:sc0d0162991d5"], "status": "REASONED"}, + "basic-auth": {"text": "Load mod_auth and mod_authn_file, select the htpasswd file backend and require valid-user Basic authentication on / over TLS; application login is preferable.", "components": ["docs"], "sources": ["docs:s1dbd1ce27da5"], "status": "REASONED"}, + "password-file": {"text": "Create the user file with Apache htpasswd (Debian apache2-utils; Fedora httpd-tools is this guide's package assessment); lighttpd 1.4.75 reads username:stored-hash entries and checks {SHA} SHA-1 and $apr1$ internally, with other hashes of at least 13 characters checked through crypt() when built with crypt support; under USE_LIB_CRYPTO_MD4, the $1+ntlm$ path derives an NTLM hash and calls crypt() with that derived value and a $1$ salt. Check the build and platform's accepted hashes before choosing a flag; this check is this guide's compatibility advice.", "components": ["docs", "authn-source", "htpasswd-tool", "debian-package", "fedora-package"], "sources": ["docs:s1dbd1ce27da5", "authn-source:s2c11c0fbc0ca", "authn-source:se971effb489f", "htpasswd-tool:scde9004bb967", "debian-package:s436bdf5f1fb9", "fedora-package:s919088d03a5e"], "status": "REASONED"}, + "mfa": {"text": "Basic authentication is single-factor; add MFA by fronting this service with Cloudflare Access or an MFA-capable proxy (this guide's requirement). Authelia v4.39.28's complete supported-proxy list has no lighttpd entry (this guide's inference from the table); Cloudflare Access documents self-hosted application and MFA policy controls; applying them to lighttpd is this guide's assessment. This guide assesses that a lighttpd origin reachable without passing through Access is not protected by it.", "components": ["docs", "authelia", "cloudflare-access"], "sources": ["docs:s1dbd1ce27da5", "authelia:s77083bc8faa2", "cloudflare-access:sfddb92e438ca", "cloudflare-access:s7cafe1b17afd", "cloudflare-access:sdcac732541da", "cloudflare-access:s603bdb6b24ab", "cloudflare-access:s863803bd9a1f"], "status": "REASONED"}, + "verify-config": {"text": "lighttpd 1.4.75 -tt -f /etc/lighttpd/lighttpd.conf tests the named file for syntax errors and initializes modules; configuration failures prevent the && reload (this guide's inference from the nonzero process result and shell chaining). The cited upstream unit starts from that file and defines a USR1 reload through ExecReload; systemctl reload triggers the service action, whose signal does not establish completion. Confirm the installed unit uses that file and defines reload (this guide's advice).", "components": ["docs", "lighttpd-verify-source", "systemd-reload"], "sources": ["docs:s6d5666b862a0", "lighttpd-verify-source:s0bbb4c861d5e", "lighttpd-verify-source:sca076bcd18dc", "lighttpd-verify-source:s9e69235fbbc7", "lighttpd-verify-source:s87104d328493", "lighttpd-verify-source:sba72bab46867", "lighttpd-verify-source:s2faa9b3149ac", "lighttpd-verify-source:s87b4b266bb0e", "lighttpd-verify-source:sa989ef425f1d", "lighttpd-verify-source:sf9d56b052c61", "systemd-reload:sb3f5069cd250", "systemd-reload:s441757b7048f"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "The HTTP header request should show a redirect to HTTPS; these are read-and-judge status checks.", "components": ["docs"], "sources": ["docs:s6de629323c64"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "An HTTPS request without credentials should return 401 once authentication is enabled.", "components": ["docs"], "sources": ["docs:s1dbd1ce27da5"], "status": "REASONED", "verify": [1]}, + "verify-listeners": {"text": "Read every listener with ss -tlnp: front-door authentication does not establish that no other listener serves content. The ss version is unrecorded.", "components": ["tls"], "sources": ["tls:sbff3cb2e01a8"], "status": "REASONED", "verify": [1]}, + "verify-host": {"text": "In lighttpd 1.4.75, request-time $HTTP[\"host\"] matching uses the HTTP Host value, while TLS SNI can select mod_openssl settings before request headers; a Host outside a conditional auth.require falls back to global configuration for that rule (this guide's inference from conditional merging and mod_auth documentation). On a protected path, this guide interprets 401 as the pass and 200 as the protected path served without authentication on this TLS connection to a client that sends another host. Apache 2.4.69 uses the SNI hostname, when the client sends it and the server supports SNI, for name-based TLS virtual-host selection; varying Host while keeping URL and SNI fixed is this guide's auth-check advice.", "components": ["docs", "lighttpd-verify-source", "apache-vhosts"], "sources": ["docs:s1dbd1ce27da5", "lighttpd-verify-source:s6a29b290d83a", "lighttpd-verify-source:s2fc4cf82abd9", "lighttpd-verify-source:s3f692a48bd01", "lighttpd-verify-source:sbb09703388c3", "lighttpd-verify-source:s8bb6bd7774f9", "lighttpd-verify-source:sb4c43fa1a807", "lighttpd-verify-source:s9e49b928cc81", "apache-vhosts:s558db45d4d61"], "status": "REASONED", "verify": [1]} + } +} +```` +====================================================================== +==> caddy.md +====================================================================== + +# Caddy: TLS and authentication + +Written against Caddy admin source v2.11.4, systemd manual v262, and Let's Encrypt expiration-email notice 2025; 7 further sources are pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-09-27; documentation checked September 2026. The guide records 31 claims, all reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + Caddy 2 obtains, installs, and renews publicly trusted certificates automatically and redirects HTTP to HTTPS by default. For a new deployment with a public domain, it is the shortest correct path to HTTPS: no ACME client, no renewal timer, no redirect block. ## 1. Public site with automatic HTTPS @@ -5978,62 +6807,240 @@ ls -l /run/caddy/admin.sock # if you bound it to a unix socket: it exis - Cloudflare Tunnel `access` setting, `cloudflared` validating the Access JWT (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104, checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/partials/cloudflare-one/tunnel/origin-parameters.mdx#L196-L215 - Docker networking documentation at 4e9a5751518ed8223a8dcde53693badddd72604f (container loopback, host namespace sharing, and the `--network container:` loopback example; the Linux bridge/host distinction above is inferred from these documented namespace semantics): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L286, https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/drivers/host.md#L11-L13, https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L302-L325 -====================================================================== -==> haproxy.md -====================================================================== - -# HAProxy: TLS termination and authentication +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/caddy.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + **Version basis** -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. -Metadata reviewed 2026-10-05; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- haproxy: HAProxy (3.0) -- hsts: HAProxy http-after-response minimum (2.2+) -- docs: Supporting documentation (unknown) -- openssl-cli: OpenSSL password command (3.0) -- certbot-source: Certbot deploy hook documentation (v2.3.0) -- cloudflare-source: Cloudflare Access MFA documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- admin: Caddy admin source (v2.11.4) +- images: Caddy Docker source (fba2853501d36e8a72f946ac8cb7ff64d07e48f2) +- dist: Default Caddyfile source (33ae08ff08d168572df2956ed14fbc4949880d94) +- docs-rolling: Caddy documentation (rolling) (unknown) +- service: caddyserver/dist service unit (33ae08ff08d168572df2956ed14fbc4949880d94) +- ratelimit-source: caddy-ratelimit source (5625512f24f6f59d6f64fb3aafe5eecff0b286db) +- tags: Official image tag map (d82ca5102fa6735be29d5e1fc6ce03af77eb091e) +- docker-network: Docker networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- systemd: systemd manual (v262) +- cloudflare: Cloudflare docs (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- letsencrypt-notice: Let's Encrypt expiration-email notice (2025) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- scope (REASONED; haproxy, docs): Examples target a maintained HAProxy 3.0 installation and treat frontend, stats, runtime API and direct backend as separate surfaces. -- pem (REASONED; haproxy): Use matching chain/key in a combined PEM, built via a root-only temporary file and rename; separate key loading is also supported. -- renewal (REASONED; haproxy, certbot-source): Rebuild the combined PEM and reload HAProxy after successful renewal using Certbot v2.3.0 --deploy-hook or an executable in /etc/letsencrypt/renewal-hooks/deploy (if the configuration directory is /etc/letsencrypt; certbot renew only). -- tls (REASONED; haproxy): Frontend :443 loads the PEM with ssl and ssl-min-ver TLSv1.2; :80 redirects to HTTPS with 301. -- ciphers (REASONED; haproxy, docs): ssl-default-bind-ciphers governs TLS 1.2 and -ciphersuites TLS 1.3; Mozilla is policy guidance, not HAProxy syntax authority. -- privileges (REASONED; haproxy): Preserve service hardening; root-started workers drop to haproxy user/group and chroot into a provisioned root-owned directory. -- timeouts (REASONED; haproxy): Set connect 5s, client/server 30s and http-request 10s timeouts. -- hsts (REASONED; haproxy, hsts): http-after-response (HAProxy 2.2+) covers generated 401/413 responses that http-response misses; includeSubDomains requires HTTPS on every affected subdomain. -- backend (REASONED; haproxy): Example backend is plaintext 127.0.0.1:3000 with health checks; keep it private. -- remote-backend (REASONED; haproxy): Remote TLS backends require certificate verification, a trusted CA and verifyhost matching the backend name. -- basic (REASONED; haproxy, openssl-cli): Use a crypt(3)-hashed userlist and http_auth condition; $6$ works on glibc Linux, while insecure-password stores plaintext. -- mtls (REASONED; haproxy): For machine access, bind verify required with ca-file requires trusted client certificates. -- mfa (REASONED; docs, cloudflare-source): Basic is single-factor; Authelia uses its HAProxy Lua integration, or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization). -- maxconn (REASONED; haproxy): Global maxconn 4096 caps per-process concurrent connections; frontend maxconn 2000 caps that frontend. -- body-guard (REASONED; haproxy): No single request-body-size directive: req.body_size reads advertised Content-Length; deny_status 413 rejects values above 10485760 without buffering. -- body-buffering (REASONED; haproxy): Do not add http-buffer-request for this guard: it waits for a complete body or full request buffer. -- chunked (REASONED; haproxy): Chunked req.body_size reflects available data, so this unbuffered guard does not meaningfully bound chunked uploads; enforce the real limit in the app. -- stats-default (REASONED; haproxy, docs): Stats is disabled until configured and enabling it adds no authentication; inventory every loaded config. -- stats-admin (REASONED; haproxy, docs): stats admin permits backend enable/disable/drain; omit it for monitoring and keep any admin page private. -- stats-auth (REASONED; haproxy, docs): Bind the example stats listener to TLS 127.0.0.1:8404 at /stats and use hashed-userlist auth instead of cleartext stats auth. -- runtime-default (REASONED; haproxy): Runtime API is not enabled by default; default command level is operator, and level admin grants authority rather than authentication. -- runtime-isolation (REASONED; haproxy): Keep /run/haproxy/admin.sock a root-owned mode-600 Unix socket; HTTP stats auth does not protect an exposed runtime TCP socket. -- verify-config (REASONED; haproxy): haproxy -c precedes reload; the guide records shell checks but no HAProxy runtime or exposed/fixed response demonstration. -- verify-redirect (REASONED; haproxy): HTTP must return 301 with an HTTPS Location; transport/TLS/DNS errors are inconclusive. -- verify-auth (REASONED; haproxy): On one protected resource, absent and wrong credentials yield 401 while valid credentials reach the expected app response; credentials travel via stdin. -- verify-size (REASONED; haproxy): With valid credentials, 1M succeeds and 11M returns 413; remove only the deny rule in isolation to attribute refusal. These Content-Length probes do not test chunked bodies. -- verify-backend (REASONED; haproxy): Inventory all listeners: backend 3000 must be loopback-only even if frontend checks pass, and the runtime endpoint must not be TCP. -- verify-stats (REASONED; haproxy, docs): Probe the 8404 stats listener itself via loopback with the certificate hostname: no credentials refused, valid credentials return statistics. -- verify-mtls (REASONED; haproxy): With verify required, no/untrusted client certificates must fail TLS and trusted ones pass; readable cert/key controls avoid local errors, and Basic may still return 401. -- verify-runtime (REASONED; haproxy): Check root/root mode 600 and root show info success versus nobody permission denial; reported admin command level is not proof of access control. +- auto-https (REASONED; docs-rolling): Caddy 2 obtains and renews public certificates and redirects HTTP to HTTPS; DNS must point here and the example requires public 80/443. +- issuers (REASONED; docs-rolling): Public certificates come from Let's Encrypt or ZeroSSL and renew automatically. +- renewal-monitoring (REASONED; docs-rolling, letsencrypt-notice): Set the ACME email contact but monitor renewal yourself: Let's Encrypt stopped expiration emails. +- backend (REASONED; docs-rolling): reverse_proxy sends traffic to 127.0.0.1:3000; keep the app private to prevent TLS/auth bypass. +- hsts (REASONED; docs-rolling): Caddy sends no HSTS automatically; add it only after HTTPS works across every included subdomain. +- forwarded (REASONED; docs-rolling): Caddy ignores untrusted client X-Forwarded-* by default; configure trusted_proxies behind a trusted CDN/proxy. +- internal-tls (REASONED; docs-rolling): tls internal uses a local CA; clients must trust its root, with caddy trust for the local host. tls also accepts explicit cert/key files. +- basic (REASONED; docs-rolling): basic_auth uses hashed passwords from caddy hash-password; it was named basicauth before v2.8.0 and remains single-factor. +- path-auth (REASONED; docs-rolling): Protect /admin and /admin/* together: path matches are exact and multiple paths are OR'ed. +- mfa (REASONED; docs-rolling, cloudflare): forward_auth (Caddy 2.5+) can delegate human authentication to Authelia; Cloudflare Access is a proposed layer that enforces MFA when its governing Access setting (policy over application over organization) requires it. +- access-origin (REASONED; docs-rolling, cloudflare): Cloudflare Access can require MFA when configured; the guide reasons that direct origin ingress can bypass Access, so restrict it; validate the Access JWT at the origin, which a tunnel's `access` setting has `cloudflared` do. Caddy still answers directly on 443. +- body-limit (REASONED; docs-rolling): Add request_body max_size 10MB inside the existing authenticated site; replacing the site without basic_auth makes it public. +- rate-limit (REASONED; docs-rolling, ratelimit-source): The standard build has no rate_limit directive; use a compiled community caddy-ratelimit module or an upstream layer. +- admin-bind (REASONED; admin, docs-rolling): The admin API defaults to localhost:2019 as of v2.11.4; never publish it or reverse-proxy to it. +- admin-authority (REASONED; docs-rolling): Reachable unauthenticated admin clients can replace config at POST /load, edit /config/ or stop with POST /stop. +- admin-isolation (REASONED; docs-rolling): Host/Origin checks are not process isolation; local untrusted workloads require a permissioned Unix socket. +- docker-admin (REASONED; tags, images, dist, admin): The official 2.11.4 Alpine, Windows Server Core 2022/2025, and Nano Server 2022/2025 runtime tags map to fba28535 Dockerfiles that EXPOSE 2019 and run the default Caddyfile without admin; absent overrides, the endpoint stays namespace-local. +- admin-override (REASONED; admin, dist, docs-rolling): A mounted admin setting or CADDY_ADMIN changes the address; do not set 0.0.0.0:2019 to make publication work. +- namespace (REASONED; images, dist, admin, docker-network, docs-rolling): The shipped admin default is localhost:2019. On Docker Engine on Linux, bridge networking keeps loopback container-local; host or container networking shares a namespace, so treat its processes as potential admin clients. +- admin-socket (REASONED; docs-rolling): admin unix//run/caddy/admin.sock uses owner-only mode 0200 by default; append |0600 to set it explicitly. +- runtime-directory (REASONED; docs-rolling, service, systemd): The dist service at 33ae08ff runs as caddy without RuntimeDirectory; systemd v262 creates /run/caddy for RuntimeDirectory=caddy at service start and owns it as caddy. Restart for the socket switch is guide reasoning. +- reload (REASONED; docs-rolling, service): The dist service at 33ae08ff uses caddy reload --config /etc/caddy/Caddyfile --force; API reload uses the Caddyfile admin address. With admin off, guide reasoning requires systemd restart. +- signal-reload (REASONED; docs-rolling): A manual run without --resume may reload startup config via SIGUSR1; API changes or reload with a different file/adapter disable signal reloads. +- on-demand (REASONED; docs-rolling): Do not enable unrestricted on_demand TLS without an ask endpoint or host allowlist; this guide does not enable it. +- verify-config (REASONED; docs-rolling): Validate /etc/caddy/Caddyfile; no validation outcome is recorded. +- verify-redirect (REASONED; docs-rolling): HTTP should redirect to HTTPS. +- verify-auth (REASONED; docs-rolling): With site-wide auth, HTTPS without credentials returns 401. +- verify-scope (REASONED; docs-rolling): The @admin variant must return 401 for /admin and /admin/x, but an app response rather than 401 for a non-admin path. +- verify-size (REASONED; docs-rolling): Authenticated 1M reaches the app and 11M returns 413; 401/000 void the control. Auth can prevent body reads; remove request_body in isolation to attribute refusal. +- verify-backend (REASONED; docs-rolling): Read all listeners: app 3000 must be loopback-only even if proxy checks pass. +- verify-admin (REASONED; docs-rolling): TCP admin must bind only loopback; absent 2019 requires confirming admin off or a socket. Check Unix socket existence and owner restrictions separately. + +````json +{ + "schema": 1, + "checked": "2026-09-27", + "documentation_checked": "2026-09", + "body_sha256": "7a5caab2cbcf90c693eab11084c5f8aebc6a3bc8e613f7ab76ca8167e3a9ea28", + "components": { + "admin": { + "name": "Caddy admin source", + "basis": "v2.11.4", + "sources": { + "s35044463fa67": "https://github.com/caddyserver/caddy/blob/v2.11.4/admin.go#L1433", + "s2bbcf239df02": "https://github.com/caddyserver/caddy/blob/v2.11.4/admin.go#L58-L67" + } + }, + "images": { + "name": "Caddy Docker source", + "basis": "fba2853501d36e8a72f946ac8cb7ff64d07e48f2", + "sources": { + "s0f0994ef9fac": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L17", + "s2dce0bbf3a70": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L59-L63", + "sd2c1b05a8088": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2022/Dockerfile#L27-L32", + "sf3e9c2d4e0cd": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2022/Dockerfile#L8", + "sc5dd3f9746fe": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2025/Dockerfile#L27-L32", + "s6c800a156eac": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows-nanoserver/ltsc2025/Dockerfile#L8", + "sc5c82704d33f": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2022/Dockerfile#L10", + "sc1cb0e1fb9eb": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2022/Dockerfile#L42-L47", + "s11528dfcc516": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2025/Dockerfile#L10", + "s70b76475a281": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/windows/ltsc2025/Dockerfile#L42-L47" + } + }, + "dist": { + "name": "Default Caddyfile source", + "basis": "33ae08ff08d168572df2956ed14fbc4949880d94", + "sources": { + "s433758a90af0": "https://github.com/caddyserver/dist/blob/33ae08ff08d168572df2956ed14fbc4949880d94/config/Caddyfile" + } + }, + "docs-rolling": { + "name": "Caddy documentation (rolling)", + "basis": "unknown", + "sources": { + "s434e7b7d7f63": "https://caddyserver.com/docs/automatic-https", + "s3923477355fb": "https://caddyserver.com/docs/api", + "sdbcd496895d6": "https://caddyserver.com/docs/caddyfile/options", + "s31af5ed45a20": "https://caddyserver.com/docs/caddyfile/directives/request_body", + "sec59a61934c5": "https://caddyserver.com/docs/caddyfile/directives", + "sbc2bd9599bd1": "https://caddyserver.com/docs/caddyfile/directives/basic_auth", + "s2e47c3139bd2": "https://caddyserver.com/docs/caddyfile/directives/tls", + "s3865a73ccda3": "https://caddyserver.com/docs/caddyfile/directives/reverse_proxy", + "s82efc39a8130": "https://caddyserver.com/docs/conventions", + "s5a6da5adf385": "https://caddyserver.com/docs/caddyfile/directives/header", + "s8df93f3e3d4c": "https://caddyserver.com/docs/caddyfile/directives/forward_auth", + "sf12ad47e81b5": "https://caddyserver.com/docs/command-line#signals", + "s38b1b78ce980": "https://caddyserver.com/docs/caddyfile/matchers" + } + }, + "service": { + "name": "caddyserver/dist service unit", + "basis": "33ae08ff08d168572df2956ed14fbc4949880d94", + "sources": { + "se70d276b03a1": "https://github.com/caddyserver/dist/blob/33ae08ff08d168572df2956ed14fbc4949880d94/init/caddy.service#L22-L32" + } + }, + "ratelimit-source": { + "name": "caddy-ratelimit source", + "basis": "5625512f24f6f59d6f64fb3aafe5eecff0b286db", + "sources": { + "s093420cd3d2f": "https://raw.githubusercontent.com/mholt/caddy-ratelimit/5625512f24f6f59d6f64fb3aafe5eecff0b286db/README.md" + } + }, + "tags": { + "name": "Official image tag map", + "basis": "d82ca5102fa6735be29d5e1fc6ce03af77eb091e", + "sources": { + "sdc8aca42f5e2": "https://github.com/docker-library/official-images/blob/d82ca5102fa6735be29d5e1fc6ce03af77eb091e/library/caddy#L7-L65" + } + }, + "docker-network": { + "name": "Docker networking documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "se1437c0f19fb": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L286", + "sfce645f8b46e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/drivers/host.md#L11-L13", + "sfd29f33e4108": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/_index.md#L302-L325" + } + }, + "systemd": { + "name": "systemd manual", + "basis": "v262", + "sources": { + "see108b0e0bba": "https://github.com/systemd/systemd/blob/v262/man/systemd.exec.xml#L1558-L1660" + } + }, + "cloudflare": { + "name": "Cloudflare docs", + "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "s6f6104362214": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L17-L85", + "s81e5dd28d3e5": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json.mdx#L15-L17", + "sa7c8d635bfd5": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/partials/cloudflare-one/tunnel/origin-parameters.mdx#L196-L215" + } + }, + "letsencrypt-notice": { + "name": "Let's Encrypt expiration-email notice", + "basis": "2025", + "sources": { + "sdf68595235d4": "https://letsencrypt.org/2025/01/22/ending-expiration-emails/" + } + } + }, + "claims": { + "auto-https": {"text": "Caddy 2 obtains and renews public certificates and redirects HTTP to HTTPS; DNS must point here and the example requires public 80/443.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "issuers": {"text": "Public certificates come from Let's Encrypt or ZeroSSL and renew automatically.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "renewal-monitoring": {"text": "Set the ACME email contact but monitor renewal yourself: Let's Encrypt stopped expiration emails.", "components": ["docs-rolling", "letsencrypt-notice"], "sources": ["docs-rolling:sdbcd496895d6", "letsencrypt-notice:sdf68595235d4"], "status": "REASONED"}, + "backend": {"text": "reverse_proxy sends traffic to 127.0.0.1:3000; keep the app private to prevent TLS/auth bypass.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED"}, + "hsts": {"text": "Caddy sends no HSTS automatically; add it only after HTTPS works across every included subdomain.", "components": ["docs-rolling"], "sources": ["docs-rolling:s5a6da5adf385", "docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "forwarded": {"text": "Caddy ignores untrusted client X-Forwarded-* by default; configure trusted_proxies behind a trusted CDN/proxy.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED"}, + "internal-tls": {"text": "tls internal uses a local CA; clients must trust its root, with caddy trust for the local host. tls also accepts explicit cert/key files.", "components": ["docs-rolling"], "sources": ["docs-rolling:s2e47c3139bd2", "docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "basic": {"text": "basic_auth uses hashed passwords from caddy hash-password; it was named basicauth before v2.8.0 and remains single-factor.", "components": ["docs-rolling"], "sources": ["docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, + "path-auth": {"text": "Protect /admin and /admin/* together: path matches are exact and multiple paths are OR'ed.", "components": ["docs-rolling"], "sources": ["docs-rolling:s38b1b78ce980", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, + "mfa": {"text": "forward_auth (Caddy 2.5+) can delegate human authentication to Authelia; Cloudflare Access is a proposed layer that enforces MFA when its governing Access setting (policy over application over organization) requires it.", "components": ["docs-rolling", "cloudflare"], "sources": ["docs-rolling:s8df93f3e3d4c", "cloudflare:s6f6104362214"], "status": "REASONED"}, + "access-origin": {"text": "Cloudflare Access can require MFA when configured; the guide reasons that direct origin ingress can bypass Access, so restrict it; validate the Access JWT at the origin, which a tunnel's `access` setting has `cloudflared` do. Caddy still answers directly on 443.", "components": ["docs-rolling", "cloudflare"], "sources": ["docs-rolling:s3865a73ccda3", "cloudflare:s6f6104362214", "cloudflare:s81e5dd28d3e5", "cloudflare:sa7c8d635bfd5"], "status": "REASONED"}, + "body-limit": {"text": "Add request_body max_size 10MB inside the existing authenticated site; replacing the site without basic_auth makes it public.", "components": ["docs-rolling"], "sources": ["docs-rolling:s31af5ed45a20", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED"}, + "rate-limit": {"text": "The standard build has no rate_limit directive; use a compiled community caddy-ratelimit module or an upstream layer.", "components": ["docs-rolling", "ratelimit-source"], "sources": ["docs-rolling:sec59a61934c5", "ratelimit-source:s093420cd3d2f"], "status": "REASONED"}, + "admin-bind": {"text": "The admin API defaults to localhost:2019 as of v2.11.4; never publish it or reverse-proxy to it.", "components": ["admin", "docs-rolling"], "sources": ["admin:s35044463fa67", "admin:s2bbcf239df02", "docs-rolling:s3923477355fb"], "status": "REASONED"}, + "admin-authority": {"text": "Reachable unauthenticated admin clients can replace config at POST /load, edit /config/ or stop with POST /stop.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb"], "status": "REASONED"}, + "admin-isolation": {"text": "Host/Origin checks are not process isolation; local untrusted workloads require a permissioned Unix socket.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "docs-rolling:sdbcd496895d6"], "status": "REASONED"}, + "docker-admin": {"text": "The official 2.11.4 Alpine, Windows Server Core 2022/2025, and Nano Server 2022/2025 runtime tags map to fba28535 Dockerfiles that EXPOSE 2019 and run the default Caddyfile without admin; absent overrides, the endpoint stays namespace-local.", "components": ["tags", "images", "dist", "admin"], "sources": ["tags:sdc8aca42f5e2", "images:s0f0994ef9fac", "images:s2dce0bbf3a70", "images:sd2c1b05a8088", "images:sf3e9c2d4e0cd", "images:s6c800a156eac", "images:sc5c82704d33f", "images:sc1cb0e1fb9eb", "images:s11528dfcc516", "images:s70b76475a281", "dist:s433758a90af0", "admin:s35044463fa67", "admin:s2bbcf239df02", "images:sc5dd3f9746fe"], "status": "REASONED"}, + "admin-override": {"text": "A mounted admin setting or CADDY_ADMIN changes the address; do not set 0.0.0.0:2019 to make publication work.", "components": ["admin", "dist", "docs-rolling"], "sources": ["docs-rolling:sdbcd496895d6", "admin:s35044463fa67", "admin:s2bbcf239df02", "dist:s433758a90af0"], "status": "REASONED"}, + "namespace": {"text": "The shipped admin default is localhost:2019. On Docker Engine on Linux, bridge networking keeps loopback container-local; host or container networking shares a namespace, so treat its processes as potential admin clients.", "components": ["images", "dist", "admin", "docker-network", "docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "images:s0f0994ef9fac", "images:s2dce0bbf3a70", "images:sd2c1b05a8088", "images:sf3e9c2d4e0cd", "images:sc5dd3f9746fe", "images:s6c800a156eac", "images:sc5c82704d33f", "images:sc1cb0e1fb9eb", "images:s11528dfcc516", "images:s70b76475a281", "dist:s433758a90af0", "admin:s35044463fa67", "docker-network:se1437c0f19fb", "docker-network:sfce645f8b46e", "docker-network:sfd29f33e4108"], "status": "REASONED"}, + "admin-socket": {"text": "admin unix//run/caddy/admin.sock uses owner-only mode 0200 by default; append |0600 to set it explicitly.", "components": ["docs-rolling"], "sources": ["docs-rolling:sdbcd496895d6", "docs-rolling:s82efc39a8130"], "status": "REASONED"}, + "runtime-directory": {"text": "The dist service at 33ae08ff runs as caddy without RuntimeDirectory; systemd v262 creates /run/caddy for RuntimeDirectory=caddy at service start and owns it as caddy. Restart for the socket switch is guide reasoning.", "components": ["docs-rolling", "service", "systemd"], "sources": ["docs-rolling:sdbcd496895d6", "service:se70d276b03a1", "systemd:see108b0e0bba"], "status": "REASONED"}, + "reload": {"text": "The dist service at 33ae08ff uses caddy reload --config /etc/caddy/Caddyfile --force; API reload uses the Caddyfile admin address. With admin off, guide reasoning requires systemd restart.", "components": ["docs-rolling", "service"], "sources": ["docs-rolling:sdbcd496895d6", "docs-rolling:sf12ad47e81b5", "service:se70d276b03a1"], "status": "REASONED"}, + "signal-reload": {"text": "A manual run without --resume may reload startup config via SIGUSR1; API changes or reload with a different file/adapter disable signal reloads.", "components": ["docs-rolling"], "sources": ["docs-rolling:sf12ad47e81b5"], "status": "REASONED"}, + "on-demand": {"text": "Do not enable unrestricted on_demand TLS without an ask endpoint or host allowlist; this guide does not enable it.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED"}, + "verify-config": {"text": "Validate /etc/caddy/Caddyfile; no validation outcome is recorded.", "components": ["docs-rolling"], "sources": ["docs-rolling:sf12ad47e81b5"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP should redirect to HTTPS.", "components": ["docs-rolling"], "sources": ["docs-rolling:s434e7b7d7f63"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "With site-wide auth, HTTPS without credentials returns 401.", "components": ["docs-rolling"], "sources": ["docs-rolling:sbc2bd9599bd1"], "status": "REASONED", "verify": [1]}, + "verify-scope": {"text": "The @admin variant must return 401 for /admin and /admin/x, but an app response rather than 401 for a non-admin path.", "components": ["docs-rolling"], "sources": ["docs-rolling:s38b1b78ce980", "docs-rolling:sbc2bd9599bd1"], "status": "REASONED", "verify": [1]}, + "verify-size": {"text": "Authenticated 1M reaches the app and 11M returns 413; 401/000 void the control. Auth can prevent body reads; remove request_body in isolation to attribute refusal.", "components": ["docs-rolling"], "sources": ["docs-rolling:s31af5ed45a20", "docs-rolling:sbc2bd9599bd1", "docs-rolling:sec59a61934c5"], "status": "REASONED", "verify": [1]}, + "verify-backend": {"text": "Read all listeners: app 3000 must be loopback-only even if proxy checks pass.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3865a73ccda3"], "status": "REASONED", "verify": [1]}, + "verify-admin": {"text": "TCP admin must bind only loopback; absent 2019 requires confirming admin off or a socket. Check Unix socket existence and owner restrictions separately.", "components": ["docs-rolling"], "sources": ["docs-rolling:s3923477355fb", "docs-rolling:sdbcd496895d6", "docs-rolling:s82efc39a8130"], "status": "REASONED", "verify": [1]} + } +} +```` +====================================================================== +==> haproxy.md +====================================================================== + +# HAProxy: TLS termination and authentication + +Written against HAProxy 3.0, HAProxy http-after-response minimum 2.2+, OpenSSL password command 3.0, and Certbot deploy hook documentation v2.3.0; 1 further source is pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-10-05; documentation checked September 2026. The guide records 30 claims, all reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + HAProxy's public frontend, its stats interface, its runtime API, and any directly reachable backend are separate exposure surfaces; protect and verify each one you enable. These examples target a maintained HAProxy 3.0 installation. Get a certificate first ([free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md)). This example combines the certificate chain and its matching private key in one PEM file (HAProxy also supports separately loaded keys): @@ -6271,67 +7278,176 @@ sudo -u nobody socat - /run/haproxy/admin.sock < /dev/null # unauthorize - Certbot v2.3.0 deploy hooks after successful renewal (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L675 - Cloudflare Access MFA documentation at c896795b8e8bab68d05ae78d64c9cbcda1fdb104, application or policy MFA (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81 -====================================================================== -==> traefik.md -====================================================================== - -# Traefik: automatic TLS and authentication middleware +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/haproxy.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + **Version basis** -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. -Metadata reviewed 2026-10-05; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- traefik-docs: Traefik documentation (rolling) (unknown) -- docker: Traefik Docker provider (unknown) -- swarm: Traefik Swarm provider (unknown) -- middleware: Traefik middleware documentation (unknown) -- curl: curl write-out minimum (7.75.0) -- traefik-v3: Traefik v3 documentation (v3.5) -- traefik-source: Traefik ACME storage source (v3.5.0) -- httpd-source: Apache httpd htpasswd source (2.4.69) -- owasp-source: OWASP Password Storage Cheat Sheet (72865b1e3b5754246a101778dc00b37f9c233373) +- haproxy: HAProxy (3.0) +- hsts: HAProxy http-after-response minimum (2.2+) +- docs: Supporting documentation (unknown) +- openssl-cli: OpenSSL password command (3.0) +- certbot-source: Certbot deploy hook documentation (v2.3.0) - cloudflare-source: Cloudflare Access MFA documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- traefik-basicauth-source: Traefik BasicAuth source (v3.5.0) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- entrypoints (REASONED; traefik-docs, traefik-v3): For Traefik v2/v3, web :80 redirects to HTTPS websecure :443 and an ACME resolver obtains/renews certificates. -- acme-storage (REASONED; traefik-docs, traefik-v3, traefik-source): Persist acme.json across restarts with mode 600 to retain certificates and avoid repeated issuance/rate limits. -- acme-challenges (REASONED; traefik-docs, traefik-v3): TLS-ALPN requires inbound 443; HTTP-01 uses 80, while DNS supports wildcards without inbound ports. -- docker-default (REASONED; docker): Docker discovery defaults exposedByDefault to true; set false and opt each intended container in with traefik.enable=true. -- docker-scope (REASONED; docker): Discovery is daemon-wide, not Compose-project scoped; eligible unlabelled containers get routers unless excluded. Missing usable ports skip service creation. -- docker-socket (REASONED; docker): The Docker socket gives daemon control despite :ro; use a restricted socket proxy to limit API access. -- tls-floor (REASONED; traefik-docs, traefik-v3): Dynamic default TLS options set minVersion VersionTLS12. -- route (REASONED; traefik-docs, docker): Docker labels select the app host, websecure, letsencrypt resolver and backend port 3000. -- backend-isolation (REASONED; docker): Publish only Traefik 80/443, not the app's 3000, to prevent middleware/TLS bypass. -- basic (REASONED; traefik-docs, traefik-v3, httpd-source, owasp-source): Attach bcrypt basicAuth to the router; Apache httpd 2.4.69 htpasswd -nB defaults to bcrypt cost 5, while -C 12 selects cost 12; OWASP at 72865b1e recommends 10 or more for legacy bcrypt. -- compose-hash (REASONED; traefik-docs, traefik-v3): Double each dollar sign in Compose hash labels; file-provider hashes need no such escaping. -- file-provider (REASONED; traefik-docs, traefik-v3): Load dynamic files with providers.file.directory/filename and reference their middleware from Docker as app-auth@file. -- mfa (REASONED; traefik-docs, traefik-v3, cloudflare-source): Basic is single-factor; use forwardAuth with Authelia/oauth2-proxy or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization). -- body-limit (REASONED; middleware): Attach buffering.maxRequestBodyBytes=10485760 alongside existing auth/routing labels; it bounds accepted body size. -- buffer-storage (REASONED; middleware): memRequestBodyBytes defaults to 1048576 and controls the memory-to-disk threshold independently of maximum accepted body size. -- rate-limit (REASONED; middleware): average=10 and burst=20 configure a rate cap, per source by default. -- inflight (REASONED; middleware): inflightreq.amount=10 bounds concurrent requests; sourceCriterion defaults to request host rather than client, so configure a per-client cap explicitly. -- middleware-order (REASONED; traefik-docs, middleware, traefik-basicauth-source): Rate/inflight precede auth to count rejected logins: Traefik v3.5.0 BasicAuth returns on failure without calling the next handler; buffering follows admission controls to avoid buffering rejected uploads. -- dashboard (REASONED; traefik-docs): Keep api.insecure and unprotected api@internal routers off public entry points or protect the dashboard with auth. -- swarm (REASONED; swarm): Standalone Docker guidance does not cover Swarm; v3 uses providers.swarm and its separate exposedByDefault also defaults true. -- verify-redirect (REASONED; traefik-docs): HTTP should redirect to HTTPS. -- verify-auth (REASONED; traefik-docs): Unauthenticated HTTPS should return 401 once auth is attached. -- verify-size (REASONED; middleware, traefik-docs): Authenticated 1M must reach the app and 11M return 413; 401/000 invalidate the control. Remove app-body in isolation to rule out backend refusal. -- observed-429 (DEMONSTRATED; middleware): The guide records only a 429 response; no limiter attribution or exposed/fixed demonstration is established. -- verify-rate (REASONED; middleware, traefik-basicauth-source): Concurrent unauthenticated probes can reach pre-auth limiters; 429 alone cannot distinguish rate, inflight or upstream refusal. Compare each limiter alone against both disabled. -- verify-isolation (REASONED; docker): Compose ps covers one project; ss misses NAT-only publication without a userland proxy. Neither finds unintended routes through Traefik. -- verify-acme (REASONED; traefik-docs): Inspect ACME startup errors; failed issuance can leave a self-signed TRAEFIK DEFAULT CERT. -- canary-route (REASONED; docker, traefik-docs): On a disposable deployment, unlabelled traefik/whoami has one HTTP port; use its generated Host rule and plain HTTP even on :443 because the generated router requests no TLS. -- verify-canary (REASONED; docker, curl): Calibrate exposedByDefault true with 200 and the canary Hostname body; false should give 404. Missing discovery/provider also gives 404, unreachable backend 502/504. -- verify-app-control (REASONED; docker, traefik-docs, curl): In the same run, the app must return 401 or known authenticated 200 while the canary gives 404; app 401 proves routing, not proxy-auth attachment. Keep TLS verification. +- scope (REASONED; haproxy, docs): Examples target a maintained HAProxy 3.0 installation and treat frontend, stats, runtime API and direct backend as separate surfaces. +- pem (REASONED; haproxy): Use matching chain/key in a combined PEM, built via a root-only temporary file and rename; separate key loading is also supported. +- renewal (REASONED; haproxy, certbot-source): Rebuild the combined PEM and reload HAProxy after successful renewal using Certbot v2.3.0 --deploy-hook or an executable in /etc/letsencrypt/renewal-hooks/deploy (if the configuration directory is /etc/letsencrypt; certbot renew only). +- tls (REASONED; haproxy): Frontend :443 loads the PEM with ssl and ssl-min-ver TLSv1.2; :80 redirects to HTTPS with 301. +- ciphers (REASONED; haproxy, docs): ssl-default-bind-ciphers governs TLS 1.2 and -ciphersuites TLS 1.3; Mozilla is policy guidance, not HAProxy syntax authority. +- privileges (REASONED; haproxy): Preserve service hardening; root-started workers drop to haproxy user/group and chroot into a provisioned root-owned directory. +- timeouts (REASONED; haproxy): Set connect 5s, client/server 30s and http-request 10s timeouts. +- hsts (REASONED; haproxy, hsts): http-after-response (HAProxy 2.2+) covers generated 401/413 responses that http-response misses; includeSubDomains requires HTTPS on every affected subdomain. +- backend (REASONED; haproxy): Example backend is plaintext 127.0.0.1:3000 with health checks; keep it private. +- remote-backend (REASONED; haproxy): Remote TLS backends require certificate verification, a trusted CA and verifyhost matching the backend name. +- basic (REASONED; haproxy, openssl-cli): Use a crypt(3)-hashed userlist and http_auth condition; $6$ works on glibc Linux, while insecure-password stores plaintext. +- mtls (REASONED; haproxy): For machine access, bind verify required with ca-file requires trusted client certificates. +- mfa (REASONED; docs, cloudflare-source): Basic is single-factor; Authelia uses its HAProxy Lua integration, or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization). +- maxconn (REASONED; haproxy): Global maxconn 4096 caps per-process concurrent connections; frontend maxconn 2000 caps that frontend. +- body-guard (REASONED; haproxy): No single request-body-size directive: req.body_size reads advertised Content-Length; deny_status 413 rejects values above 10485760 without buffering. +- body-buffering (REASONED; haproxy): Do not add http-buffer-request for this guard: it waits for a complete body or full request buffer. +- chunked (REASONED; haproxy): Chunked req.body_size reflects available data, so this unbuffered guard does not meaningfully bound chunked uploads; enforce the real limit in the app. +- stats-default (REASONED; haproxy, docs): Stats is disabled until configured and enabling it adds no authentication; inventory every loaded config. +- stats-admin (REASONED; haproxy, docs): stats admin permits backend enable/disable/drain; omit it for monitoring and keep any admin page private. +- stats-auth (REASONED; haproxy, docs): Bind the example stats listener to TLS 127.0.0.1:8404 at /stats and use hashed-userlist auth instead of cleartext stats auth. +- runtime-default (REASONED; haproxy): Runtime API is not enabled by default; default command level is operator, and level admin grants authority rather than authentication. +- runtime-isolation (REASONED; haproxy): Keep /run/haproxy/admin.sock a root-owned mode-600 Unix socket; HTTP stats auth does not protect an exposed runtime TCP socket. +- verify-config (REASONED; haproxy): haproxy -c precedes reload; the guide records shell checks but no HAProxy runtime or exposed/fixed response demonstration. +- verify-redirect (REASONED; haproxy): HTTP must return 301 with an HTTPS Location; transport/TLS/DNS errors are inconclusive. +- verify-auth (REASONED; haproxy): On one protected resource, absent and wrong credentials yield 401 while valid credentials reach the expected app response; credentials travel via stdin. +- verify-size (REASONED; haproxy): With valid credentials, 1M succeeds and 11M returns 413; remove only the deny rule in isolation to attribute refusal. These Content-Length probes do not test chunked bodies. +- verify-backend (REASONED; haproxy): Inventory all listeners: backend 3000 must be loopback-only even if frontend checks pass, and the runtime endpoint must not be TCP. +- verify-stats (REASONED; haproxy, docs): Probe the 8404 stats listener itself via loopback with the certificate hostname: no credentials refused, valid credentials return statistics. +- verify-mtls (REASONED; haproxy): With verify required, no/untrusted client certificates must fail TLS and trusted ones pass; readable cert/key controls avoid local errors, and Basic may still return 401. +- verify-runtime (REASONED; haproxy): Check root/root mode 600 and root show info success versus nobody permission denial; reported admin command level is not proof of access control. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "afa78827cbab9d1fd7020200e3a50b28da524fc31cba3550243a014cbe4363a9", + "components": { + "haproxy": { + "name": "HAProxy", + "basis": "3.0", + "sources": { + "s5792e6ff4990": "https://docs.haproxy.org/3.0/configuration.html", + "s9c32621d6493": "https://docs.haproxy.org/3.0/management.html" + } + }, + "hsts": { + "name": "HAProxy http-after-response minimum", + "basis": "2.2+", + "sources": { + "s5792e6ff4990": "https://docs.haproxy.org/3.0/configuration.html" + } + }, + "docs": { + "name": "Supporting documentation", + "basis": "unknown", + "sources": { + "s2be90eb936bd": "https://www.haproxy.org/", + "sae992a8ac3a7": "https://www.haproxy.com/documentation/haproxy-configuration-tutorials/alerts-and-monitoring/statistics/", + "s192d42dd9feb": "https://www.authelia.com/integration/proxies/haproxy/", + "s529b0eabe2ed": "https://ssl-config.mozilla.org/" + } + }, + "openssl-cli": { + "name": "OpenSSL password command", + "basis": "3.0", + "sources": { + "s3b2ccc45875a": "https://docs.openssl.org/3.0/man1/openssl-passwd/" + } + }, + "certbot-source": { + "name": "Certbot deploy hook documentation", + "basis": "v2.3.0", + "sources": { + "sd39028a759a7": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L675" + } + }, + "cloudflare-source": { + "name": "Cloudflare Access MFA documentation", + "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" + } + } + }, + "claims": { + "scope": {"text": "Examples target a maintained HAProxy 3.0 installation and treat frontend, stats, runtime API and direct backend as separate surfaces.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493", "docs:s2be90eb936bd"], "status": "REASONED"}, + "pem": {"text": "Use matching chain/key in a combined PEM, built via a root-only temporary file and rename; separate key loading is also supported.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "renewal": {"text": "Rebuild the combined PEM and reload HAProxy after successful renewal using Certbot v2.3.0 --deploy-hook or an executable in /etc/letsencrypt/renewal-hooks/deploy (if the configuration directory is /etc/letsencrypt; certbot renew only).", "components": ["haproxy", "certbot-source"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493", "certbot-source:sd39028a759a7"], "status": "REASONED"}, + "tls": {"text": "Frontend :443 loads the PEM with ssl and ssl-min-ver TLSv1.2; :80 redirects to HTTPS with 301.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "ciphers": {"text": "ssl-default-bind-ciphers governs TLS 1.2 and -ciphersuites TLS 1.3; Mozilla is policy guidance, not HAProxy syntax authority.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:s529b0eabe2ed"], "status": "REASONED"}, + "privileges": {"text": "Preserve service hardening; root-started workers drop to haproxy user/group and chroot into a provisioned root-owned directory.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "timeouts": {"text": "Set connect 5s, client/server 30s and http-request 10s timeouts.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "hsts": {"text": "http-after-response (HAProxy 2.2+) covers generated 401/413 responses that http-response misses; includeSubDomains requires HTTPS on every affected subdomain.", "components": ["haproxy", "hsts"], "sources": ["haproxy:s5792e6ff4990", "hsts:s5792e6ff4990"], "status": "REASONED"}, + "backend": {"text": "Example backend is plaintext 127.0.0.1:3000 with health checks; keep it private.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "remote-backend": {"text": "Remote TLS backends require certificate verification, a trusted CA and verifyhost matching the backend name.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "basic": {"text": "Use a crypt(3)-hashed userlist and http_auth condition; $6$ works on glibc Linux, while insecure-password stores plaintext.", "components": ["haproxy", "openssl-cli"], "sources": ["haproxy:s5792e6ff4990", "openssl-cli:s3b2ccc45875a"], "status": "REASONED"}, + "mtls": {"text": "For machine access, bind verify required with ca-file requires trusted client certificates.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "mfa": {"text": "Basic is single-factor; Authelia uses its HAProxy Lua integration, or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization).", "components": ["docs", "cloudflare-source"], "sources": ["docs:s192d42dd9feb", "cloudflare-source:sdcac732541da"], "status": "REASONED"}, + "maxconn": {"text": "Global maxconn 4096 caps per-process concurrent connections; frontend maxconn 2000 caps that frontend.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "body-guard": {"text": "No single request-body-size directive: req.body_size reads advertised Content-Length; deny_status 413 rejects values above 10485760 without buffering.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "body-buffering": {"text": "Do not add http-buffer-request for this guard: it waits for a complete body or full request buffer.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "chunked": {"text": "Chunked req.body_size reflects available data, so this unbuffered guard does not meaningfully bound chunked uploads; enforce the real limit in the app.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"}, + "stats-default": {"text": "Stats is disabled until configured and enabling it adds no authentication; inventory every loaded config.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, + "stats-admin": {"text": "stats admin permits backend enable/disable/drain; omit it for monitoring and keep any admin page private.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, + "stats-auth": {"text": "Bind the example stats listener to TLS 127.0.0.1:8404 at /stats and use hashed-userlist auth instead of cleartext stats auth.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"}, + "runtime-default": {"text": "Runtime API is not enabled by default; default command level is operator, and level admin grants authority rather than authentication.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED"}, + "runtime-isolation": {"text": "Keep /run/haproxy/admin.sock a root-owned mode-600 Unix socket; HTTP stats auth does not protect an exposed runtime TCP socket.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED"}, + "verify-config": {"text": "haproxy -c precedes reload; the guide records shell checks but no HAProxy runtime or exposed/fixed response demonstration.", "components": ["haproxy"], "sources": ["haproxy:s9c32621d6493"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP must return 301 with an HTTPS Location; transport/TLS/DNS errors are inconclusive.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "On one protected resource, absent and wrong credentials yield 401 while valid credentials reach the expected app response; credentials travel via stdin.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, + "verify-size": {"text": "With valid credentials, 1M succeeds and 11M returns 413; remove only the deny rule in isolation to attribute refusal. These Content-Length probes do not test chunked bodies.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]}, + "verify-backend": {"text": "Inventory all listeners: backend 3000 must be loopback-only even if frontend checks pass, and the runtime endpoint must not be TCP.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED", "verify": [1]}, + "verify-stats": {"text": "Probe the 8404 stats listener itself via loopback with the certificate hostname: no credentials refused, valid credentials return statistics.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED", "verify": [2]}, + "verify-mtls": {"text": "With verify required, no/untrusted client certificates must fail TLS and trusted ones pass; readable cert/key controls avoid local errors, and Basic may still return 401.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [2]}, + "verify-runtime": {"text": "Check root/root mode 600 and root show info success versus nobody permission denial; reported admin command level is not proof of access control.", "components": ["haproxy"], "sources": ["haproxy:s9c32621d6493", "haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [2]} + } +} +```` +====================================================================== +==> traefik.md +====================================================================== + +# Traefik: automatic TLS and authentication middleware + +Written against Traefik v3 documentation v3.5, Traefik ACME storage source v3.5.0, Traefik BasicAuth source v3.5.0, Apache httpd htpasswd source 2.4.69, and curl write-out minimum 7.75.0; 2 further sources are pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-10-05; documentation checked September 2026. The guide records 30 claims: 1 demonstrated by a check that ran and 29 reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + Applies to Traefik v2 and v3. Traefik obtains and renews certificates itself through ACME resolvers, which suits container deployments. ## 1. Static configuration: entry points, redirect, ACME @@ -6582,6 +7698,212 @@ With the authentication from section 3 in place, this unauthenticated probe retu - OWASP Password Storage Cheat Sheet at 72865b1e3b5754246a101778dc00b37f9c233373, legacy bcrypt work factor (checked October 2026): https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13 - Cloudflare Access MFA documentation at c896795b8e8bab68d05ae78d64c9cbcda1fdb104, application or policy MFA (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81 + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/traefik.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- traefik-v3: Traefik v3 documentation (v3.5) +- traefik-source: Traefik ACME storage source (v3.5.0) +- traefik-basicauth-source: Traefik BasicAuth source (v3.5.0) +- traefik-docs: Traefik documentation (rolling) (unknown) +- docker: Traefik Docker provider (unknown) +- swarm: Traefik Swarm provider (unknown) +- middleware: Traefik middleware documentation (unknown) +- httpd-source: Apache httpd htpasswd source (2.4.69) +- cloudflare-source: Cloudflare Access MFA documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- curl: curl write-out minimum (7.75.0) +- owasp-source: OWASP Password Storage Cheat Sheet (72865b1e3b5754246a101778dc00b37f9c233373) + +Claims (full source mapping in the JSON block below): + +- entrypoints (REASONED; traefik-docs, traefik-v3): For Traefik v2/v3, web :80 redirects to HTTPS websecure :443 and an ACME resolver obtains/renews certificates. +- acme-storage (REASONED; traefik-docs, traefik-v3, traefik-source): Persist acme.json across restarts with mode 600 to retain certificates and avoid repeated issuance/rate limits. +- acme-challenges (REASONED; traefik-docs, traefik-v3): TLS-ALPN requires inbound 443; HTTP-01 uses 80, while DNS supports wildcards without inbound ports. +- docker-default (REASONED; docker): Docker discovery defaults exposedByDefault to true; set false and opt each intended container in with traefik.enable=true. +- docker-scope (REASONED; docker): Discovery is daemon-wide, not Compose-project scoped; eligible unlabelled containers get routers unless excluded. Missing usable ports skip service creation. +- docker-socket (REASONED; docker): The Docker socket gives daemon control despite :ro; use a restricted socket proxy to limit API access. +- tls-floor (REASONED; traefik-docs, traefik-v3): Dynamic default TLS options set minVersion VersionTLS12. +- route (REASONED; traefik-docs, docker): Docker labels select the app host, websecure, letsencrypt resolver and backend port 3000. +- backend-isolation (REASONED; docker): Publish only Traefik 80/443, not the app's 3000, to prevent middleware/TLS bypass. +- basic (REASONED; traefik-docs, traefik-v3, httpd-source, owasp-source): Attach bcrypt basicAuth to the router; Apache httpd 2.4.69 htpasswd -nB defaults to bcrypt cost 5, while -C 12 selects cost 12; OWASP at 72865b1e recommends 10 or more for legacy bcrypt. +- compose-hash (REASONED; traefik-docs, traefik-v3): Double each dollar sign in Compose hash labels; file-provider hashes need no such escaping. +- file-provider (REASONED; traefik-docs, traefik-v3): Load dynamic files with providers.file.directory/filename and reference their middleware from Docker as app-auth@file. +- mfa (REASONED; traefik-docs, traefik-v3, cloudflare-source): Basic is single-factor; use forwardAuth with Authelia/oauth2-proxy or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization). +- body-limit (REASONED; middleware): Attach buffering.maxRequestBodyBytes=10485760 alongside existing auth/routing labels; it bounds accepted body size. +- buffer-storage (REASONED; middleware): memRequestBodyBytes defaults to 1048576 and controls the memory-to-disk threshold independently of maximum accepted body size. +- rate-limit (REASONED; middleware): average=10 and burst=20 configure a rate cap, per source by default. +- inflight (REASONED; middleware): inflightreq.amount=10 bounds concurrent requests; sourceCriterion defaults to request host rather than client, so configure a per-client cap explicitly. +- middleware-order (REASONED; traefik-docs, middleware, traefik-basicauth-source): Rate/inflight precede auth to count rejected logins: Traefik v3.5.0 BasicAuth returns on failure without calling the next handler; buffering follows admission controls to avoid buffering rejected uploads. +- dashboard (REASONED; traefik-docs): Keep api.insecure and unprotected api@internal routers off public entry points or protect the dashboard with auth. +- swarm (REASONED; swarm): Standalone Docker guidance does not cover Swarm; v3 uses providers.swarm and its separate exposedByDefault also defaults true. +- verify-redirect (REASONED; traefik-docs): HTTP should redirect to HTTPS. +- verify-auth (REASONED; traefik-docs): Unauthenticated HTTPS should return 401 once auth is attached. +- verify-size (REASONED; middleware, traefik-docs): Authenticated 1M must reach the app and 11M return 413; 401/000 invalidate the control. Remove app-body in isolation to rule out backend refusal. +- observed-429 (DEMONSTRATED; middleware): The guide records only a 429 response; no limiter attribution or exposed/fixed demonstration is established. +- verify-rate (REASONED; middleware, traefik-basicauth-source): Concurrent unauthenticated probes can reach pre-auth limiters; 429 alone cannot distinguish rate, inflight or upstream refusal. Compare each limiter alone against both disabled. +- verify-isolation (REASONED; docker): Compose ps covers one project; ss misses NAT-only publication without a userland proxy. Neither finds unintended routes through Traefik. +- verify-acme (REASONED; traefik-docs): Inspect ACME startup errors; failed issuance can leave a self-signed TRAEFIK DEFAULT CERT. +- canary-route (REASONED; docker, traefik-docs): On a disposable deployment, unlabelled traefik/whoami has one HTTP port; use its generated Host rule and plain HTTP even on :443 because the generated router requests no TLS. +- verify-canary (REASONED; docker, curl): Calibrate exposedByDefault true with 200 and the canary Hostname body; false should give 404. Missing discovery/provider also gives 404, unreachable backend 502/504. +- verify-app-control (REASONED; docker, traefik-docs, curl): In the same run, the app must return 401 or known authenticated 200 while the canary gives 404; app 401 proves routing, not proxy-auth attachment. Keep TLS verification. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "aac661357effc89cf9b208294d0f255b4fb31c03428609d9756c4fa69347809c", + "components": { + "traefik-v3": { + "name": "Traefik v3 documentation", + "basis": "v3.5", + "sources": { + "sd6ad276040b7": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/entrypoints/", + "s418a6b1ad09b": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/tls/certificate-resolvers/acme/", + "s25d849b2d428": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/tls/tls-options/", + "s3acb3f5b1778": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/providers/others/file/", + "s5d7a6940f1dd": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/middlewares/basicauth/", + "s3cf7d994a56a": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/middlewares/forwardauth/" + } + }, + "traefik-source": { + "name": "Traefik ACME storage source", + "basis": "v3.5.0", + "sources": { + "s0e08b0bdcb35": "https://github.com/traefik/traefik/blob/v3.5.0/pkg/provider/acme/local_store.go#L125" + } + }, + "traefik-basicauth-source": { + "name": "Traefik BasicAuth source", + "basis": "v3.5.0", + "sources": { + "s5f9a13d720c8": "https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/basic_auth.go#L67-L99" + } + }, + "traefik-docs": { + "name": "Traefik documentation (rolling)", + "basis": "unknown", + "sources": { + "scbc0f2135c78": "https://doc.traefik.io/traefik/" + } + }, + "docker": { + "name": "Traefik Docker provider", + "basis": "unknown", + "sources": { + "se23fe4fe79fa": "https://doc.traefik.io/traefik/reference/install-configuration/providers/docker/" + } + }, + "swarm": { + "name": "Traefik Swarm provider", + "basis": "unknown", + "sources": { + "s256d46f3f558": "https://doc.traefik.io/traefik/reference/install-configuration/providers/swarm/" + } + }, + "middleware": { + "name": "Traefik middleware documentation", + "basis": "unknown", + "sources": { + "sd987195d64dd": "https://doc.traefik.io/traefik/middlewares/http/buffering/", + "sd20dcba7ebca": "https://doc.traefik.io/traefik/middlewares/http/inflightreq/", + "scf1b1e35ef5d": "https://doc.traefik.io/traefik/middlewares/http/ratelimit/" + } + }, + "httpd-source": { + "name": "Apache httpd htpasswd source", + "basis": "2.4.69", + "sources": { + "sf711558bdf3c": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L45", + "s4aa0f7f341fb": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L292-L295", + "s4482c55d79a2": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L382", + "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128" + } + }, + "cloudflare-source": { + "name": "Cloudflare Access MFA documentation", + "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" + } + }, + "curl": { + "name": "curl write-out minimum", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + }, + "owasp-source": { + "name": "OWASP Password Storage Cheat Sheet", + "basis": "72865b1e3b5754246a101778dc00b37f9c233373", + "sources": { + "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" + } + } + }, + "claims": { + "entrypoints": {"text": "For Traefik v2/v3, web :80 redirects to HTTPS websecure :443 and an ACME resolver obtains/renews certificates.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:sd6ad276040b7", "traefik-v3:s418a6b1ad09b"], "status": "REASONED"}, + "acme-storage": {"text": "Persist acme.json across restarts with mode 600 to retain certificates and avoid repeated issuance/rate limits.", "components": ["traefik-docs", "traefik-v3", "traefik-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s418a6b1ad09b", "traefik-source:s0e08b0bdcb35"], "status": "REASONED"}, + "acme-challenges": {"text": "TLS-ALPN requires inbound 443; HTTP-01 uses 80, while DNS supports wildcards without inbound ports.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s418a6b1ad09b"], "status": "REASONED"}, + "docker-default": {"text": "Docker discovery defaults exposedByDefault to true; set false and opt each intended container in with traefik.enable=true.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, + "docker-scope": {"text": "Discovery is daemon-wide, not Compose-project scoped; eligible unlabelled containers get routers unless excluded. Missing usable ports skip service creation.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, + "docker-socket": {"text": "The Docker socket gives daemon control despite :ro; use a restricted socket proxy to limit API access.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, + "tls-floor": {"text": "Dynamic default TLS options set minVersion VersionTLS12.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s25d849b2d428"], "status": "REASONED"}, + "route": {"text": "Docker labels select the app host, websecure, letsencrypt resolver and backend port 3000.", "components": ["traefik-docs", "docker"], "sources": ["traefik-docs:scbc0f2135c78", "docker:se23fe4fe79fa"], "status": "REASONED"}, + "backend-isolation": {"text": "Publish only Traefik 80/443, not the app's 3000, to prevent middleware/TLS bypass.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, + "basic": {"text": "Attach bcrypt basicAuth to the router; Apache httpd 2.4.69 htpasswd -nB defaults to bcrypt cost 5, while -C 12 selects cost 12; OWASP at 72865b1e recommends 10 or more for legacy bcrypt.", "components": ["traefik-docs", "traefik-v3", "httpd-source", "owasp-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s5d7a6940f1dd", "httpd-source:sf711558bdf3c", "httpd-source:s4aa0f7f341fb", "httpd-source:s4482c55d79a2", "httpd-source:s309ecdd34379", "owasp-source:s4531e65b6502"], "status": "REASONED"}, + "compose-hash": {"text": "Double each dollar sign in Compose hash labels; file-provider hashes need no such escaping.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s5d7a6940f1dd"], "status": "REASONED"}, + "file-provider": {"text": "Load dynamic files with providers.file.directory/filename and reference their middleware from Docker as app-auth@file.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s3acb3f5b1778"], "status": "REASONED"}, + "mfa": {"text": "Basic is single-factor; use forwardAuth with Authelia/oauth2-proxy or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization).", "components": ["traefik-docs", "traefik-v3", "cloudflare-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s3cf7d994a56a", "cloudflare-source:sdcac732541da"], "status": "REASONED"}, + "body-limit": {"text": "Attach buffering.maxRequestBodyBytes=10485760 alongside existing auth/routing labels; it bounds accepted body size.", "components": ["middleware"], "sources": ["middleware:sd987195d64dd"], "status": "REASONED"}, + "buffer-storage": {"text": "memRequestBodyBytes defaults to 1048576 and controls the memory-to-disk threshold independently of maximum accepted body size.", "components": ["middleware"], "sources": ["middleware:sd987195d64dd"], "status": "REASONED"}, + "rate-limit": {"text": "average=10 and burst=20 configure a rate cap, per source by default.", "components": ["middleware"], "sources": ["middleware:scf1b1e35ef5d"], "status": "REASONED"}, + "inflight": {"text": "inflightreq.amount=10 bounds concurrent requests; sourceCriterion defaults to request host rather than client, so configure a per-client cap explicitly.", "components": ["middleware"], "sources": ["middleware:sd20dcba7ebca"], "status": "REASONED"}, + "middleware-order": {"text": "Rate/inflight precede auth to count rejected logins: Traefik v3.5.0 BasicAuth returns on failure without calling the next handler; buffering follows admission controls to avoid buffering rejected uploads.", "components": ["traefik-docs", "middleware", "traefik-basicauth-source"], "sources": ["traefik-docs:scbc0f2135c78", "middleware:sd987195d64dd", "middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca", "traefik-basicauth-source:s5f9a13d720c8"], "status": "REASONED"}, + "dashboard": {"text": "Keep api.insecure and unprotected api@internal routers off public entry points or protect the dashboard with auth.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED"}, + "swarm": {"text": "Standalone Docker guidance does not cover Swarm; v3 uses providers.swarm and its separate exposedByDefault also defaults true.", "components": ["swarm"], "sources": ["swarm:s256d46f3f558"], "status": "REASONED"}, + "verify-redirect": {"text": "HTTP should redirect to HTTPS.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "Unauthenticated HTTPS should return 401 once auth is attached.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, + "verify-size": {"text": "Authenticated 1M must reach the app and 11M return 413; 401/000 invalidate the control. Remove app-body in isolation to rule out backend refusal.", "components": ["middleware", "traefik-docs"], "sources": ["middleware:sd987195d64dd", "traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, + "observed-429": {"text": "The guide records only a 429 response; no limiter attribution or exposed/fixed demonstration is established.", "components": ["middleware"], "sources": ["middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca"], "status": "DEMONSTRATED", "evidence": "A 429 appeared. That is all this shows."}, + "verify-rate": {"text": "Concurrent unauthenticated probes can reach pre-auth limiters; 429 alone cannot distinguish rate, inflight or upstream refusal. Compare each limiter alone against both disabled.", "components": ["middleware", "traefik-basicauth-source"], "sources": ["middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca", "traefik-basicauth-source:s5f9a13d720c8"], "status": "REASONED", "verify": [1]}, + "verify-isolation": {"text": "Compose ps covers one project; ss misses NAT-only publication without a userland proxy. Neither finds unintended routes through Traefik.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED", "verify": [1]}, + "verify-acme": {"text": "Inspect ACME startup errors; failed issuance can leave a self-signed TRAEFIK DEFAULT CERT.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, + "canary-route": {"text": "On a disposable deployment, unlabelled traefik/whoami has one HTTP port; use its generated Host rule and plain HTTP even on :443 because the generated router requests no TLS.", "components": ["docker", "traefik-docs"], "sources": ["docker:se23fe4fe79fa", "traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [2]}, + "verify-canary": {"text": "Calibrate exposedByDefault true with 200 and the canary Hostname body; false should give 404. Missing discovery/provider also gives 404, unreachable backend 502/504.", "components": ["docker", "curl"], "sources": ["docker:se23fe4fe79fa", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]}, + "verify-app-control": {"text": "In the same run, the app must return 401 or known authenticated 200 while the canary gives 404; app 401 proves routing, not proxy-auth attachment. Keep TLS verification.", "components": ["docker", "traefik-docs", "curl"], "sources": ["docker:se23fe4fe79fa", "traefik-docs:scbc0f2135c78", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [3]} + } +} +```` + + ====================================================================== ==> nodejs.md ====================================================================== @@ -7065,71 +8387,9 @@ ss -tlnp # every listener: loopback for a same-host reverse proxy, or the plat # Docker and Compose: exposure, TLS, and authentication - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-18; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- docker-build: Docker build and image history documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- engine: Docker Engine networking (unknown) -- engine-pinned: Docker Engine networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- moby-unit: Moby Docker service unit (v28.3.3) -- systemd-unit: systemd unit ordering (v262) -- boundary: Docker Engine minimum boundary (28.0) -- compose: Compose networking (unknown) -- git-ignore: Git ignore rules (v2.49.0) -- docker-workload: Docker workload documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- reload: Docker firewalld reload fix (28.3.3) -- reload-advisory: Docker firewalld reload advisory (unknown) -- postgresql: PostgreSQL (17) -- caddy-image: Caddy image (fba2853501d36e8a72f946ac8cb7ff64d07e48f2) -- caddy: Caddy (v2.11.4) -- nginx-auth: nginx authentication source (release-1.26.3) -- traefik-auth: Traefik authentication source (v3.5.0) -- cloudflare-access: Cloudflare Access documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- authelia-proxy: Authelia proxy integration (v4.39.28) -- aws-firewall: AWS security group rules (unknown) -- gcp-firewall: Google Cloud VPC firewall rules (unknown) -- azure-firewall: Azure network security group rules (unknown) -- cloudflared-image: cloudflared container image (2026.9.3) -- cloudflare-tunnel: Cloudflare Tunnel documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- compose-pinned: Docker Compose networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- tailscale-entry: Tailscale entry documentation (unknown) -- self-signed-entry: OpenSSL documentation (rolling) (unknown) - -Claims (full source mapping in the front matter): +Written against Docker Engine minimum boundary 28.0, Moby Docker service unit v28.3.3, Docker firewalld reload fix 28.3.3, systemd unit ordering v262, Git ignore rules v2.49.0, PostgreSQL 17, Caddy v2.11.4, and nginx authentication source release-1.26.3, plus 3 more recorded bases; 7 further sources are pinned to a commit and 8 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 26 claims, all reasoned from cited sources. -- publish (REASONED; engine): Omitting the host address publishes 3000:3000 on 0.0.0.0 and [::]. -- host-firewall (REASONED; engine): Docker programs firewall rules directly; UFW or firewalld blocking a host port does not establish published-port isolation. -- private-network (REASONED; compose, postgresql): Omit app/database ports; Compose peers use service names. The example uses postgres:17 at db:5432. -- loopback (REASONED; engine, boundary, reload, reload-advisory): Publish local access as 127.0.0.1:3000:3000; check the running server version. Engine 28.0 is the minimum boundary, with 28.3.3 or later required where firewalld is used. -- old-loopback (REASONED; boundary): Before 28.0, same-L2 neighbours could reach loopback publications under the default bridge configuration. -- old-host-bind (REASONED; boundary): Before 28.0, remote hosts could reach published container ports despite the host-IP binding. -- old-unpublished (REASONED; boundary): Before 28.0, direct routing could reach unpublished container ports; 28.0 fixed the stated default-bridge exposures. -- upstream (REASONED; engine, aws-firewall, gcp-firewall, azure-firewall): Use an upstream security group or network ACL for routable publications and boot-time protection. AWS security groups, Google Cloud VPC firewall rules and Azure NSGs have rolling sources; network-ACL configuration and boot-time independence remain this guide's assessment. -- docker-user (REASONED; engine): On the iptables backend, DOCKER-USER precedes Docker accepts; the nftables backend has no such chain. Match NEW traffic from outside the allowed subnet. -- dnat (REASONED; engine): DOCKER-USER sees post-DNAT container destinations; conntrack original destination port and ORIGINAL direction scope a published service. -- ipv6 (REASONED; engine, engine-pinned): As this guide's reasoning, native IPv6 forwarding needs ip6tables; Docker documents IPv6-to-IPv4 userland-proxy mapping when no host IP is given, the bridge is IPv4-only and `--userland-proxy=true` (the default), and places DOCKER-USER in FORWARD. As this guide's reasoning, the proxy path terminates on host INPUT and bypasses DOCKER-USER. -- proxy-path (REASONED; engine, engine-pinned): Close the userland-proxy path with an explicit publish address, disabled userland proxy or IPv6 INPUT restrictions; test both address families and sources. Docker documents the explicit-address option and conditional userland-proxy mapping; the INPUT restriction and probes are this guide's reasoning and advice. -- persistence (REASONED; engine, engine-pinned, moby-unit, systemd-unit): As this guide's advice, persist only DOCKER-USER rules after Docker startup, avoiding blanket dynamic-chain saves. Moby identifies docker.service and systemd documents After= ordering; as this guide's assessment, rule non-persistence, the boot-time gap, what iptables-persistent captures and its potential to break container networking have no direct source here. -- tls (REASONED; compose, caddy-image, caddy): Example caddy:2 publishes 80:80 and 443:443, mounts Caddyfile read-only and persists data/config, proxying app:3000 with automatic certificates. -- alternate-entry (REASONED; compose, cloudflared-image, cloudflare-tunnel, compose-pinned, tailscale-entry, self-signed-entry): As this guide's advice, hosts with no inbound ports and a domain they can put on Cloudflare use a cloudflared container, while hosts with no domain at all use Tailscale or internal self-signed TLS. Cloudflare documents a tunnel daemon that needs no firewall openings, a container image, an HTTPS origin service URL and a domain prerequisite; Docker documents Compose service-name lookup. The http://app:3000 origin target on a shared Compose network is this guide's inference, not shown literally by the fetched Cloudflare lines. The tunnel-token setup is not shown here; the Tailscale and OpenSSL references are rolling, and the self-signed fallback remains this guide's advice. -- auth (REASONED; compose, caddy, nginx-auth, traefik-auth, cloudflare-access, authelia-proxy): As this guide's assessment, proxy authentication and human MFA supplement application login. Caddy and nginx basic authentication reject missing credentials; nginx auth_request returns 401/403; Traefik basicAuth calls RequireAuth and forwardAuth passes through non-2xx; Cloudflare Access policies can require MFA, and Authelia can sit behind the proxy. Require MFA in the governing policy or portal; as this guide's assessment, basic auth alone is single-factor. The Caddy forward_auth rejection status, exact Traefik basicAuth status and Cloudflare Access anonymous status are not established by these fetched lines. -- secrets (REASONED; compose, docker-build): Use runtime environment files or Compose secrets, exclude .env from Git, and never put secrets in Dockerfile ENV/ARG; use temporary build secret mounts. Docker build and history behaviour is sourced; as this guide's assessment, the cited history reference does not establish that every argument value is shown; as this guide's assessment, never bake secrets into images and do not copy or print build-time secrets into the image. No direct source for the runtime delivery methods or the .env exclusion from Git is listed. -- workload (REASONED; compose, git-ignore, docker-workload): Keep .env in .gitignore, which ignores matching untracked files but not tracked files. Use a non-root Dockerfile USER; as this guide's assessment, a compromised app then is not root in the container. Drop capabilities, use no-new-privileges and a read-only root filesystem, and never mount the Docker socket. Docker sources establish the option names, that only trusted users should control the daemon, and that Unix permissions can limit access to its control socket; the linked guide's hardening consequences, including the mounted-socket path, remain this guide's assessment. -- database (REASONED; compose): Database TLS and authentication remain necessary for connections from outside the Compose network; database vendor sources are in linked guides. -- verify-publish (REASONED; compose, engine): Compose ps is project-scoped; inventory every host container and TCP/UDP socket, including IPv6. Socket visibility alone does not prove reachability. -- verify-redirect (REASONED; compose, caddy): Verify that HTTP redirects to HTTPS: Caddy v2.11.4 configures an automatic HTTPS redirect; as this guide's configuration-dependent expectation, nginx and Traefik require a configured redirect. This is an expectation, not an observed deployment result. -- verify-auth (REASONED; compose, caddy, nginx-auth, traefik-auth, authelia-proxy, cloudflare-access): Verify unauthenticated HTTPS /api rejection with printed status and redirect location; the probe itself fails on any 2xx. A 3xx passes only when the location is the gate's own login portal, never the application's own login; verify authorized success separately. Caddy basic auth and nginx auth_basic expect 401; Caddy forward_auth admits auth-service 2xx, but its rejection status is not established by the fetched lines. nginx auth_request returns 401 or 403, while the cited Authelia nginx configuration turns 401 into 302 to its portal. Traefik forwardAuth passes through a rejecting auth service's status, including a login redirect; Traefik basicAuth's 401 is this guide's expectation because the fetched code calls RequireAuth, whose response these lines do not show. Cloudflare Access's anonymous 302 login redirect is this guide's assessment per cloudflare.md, not established by the fetched Cloudflare lines. -- verify-isolation (REASONED; engine): Probe each restricted host address and publication from allowed/disallowed sources in both families; backend isolation must coexist with proxy reachability. -- firewalld-reload (REASONED; reload, reload-advisory): Engine 28.2.x and 28.3.0 through 28.3.2 fail to restore container-address filtering after firewalld reload; 28.3.3 fixes CVE-2025-54388 (GHSA-x4rx-4gw3-53p4). -- firewalld-scope (REASONED; reload, reload-advisory): The regression affects Linux Engine in the host network namespace with firewalld: routed remote hosts can reach published container ports, including loopback publications; unpublished ports stay filtered. Rootless Mode and Docker Desktop are unaffected. -- verify-firewalld-reload (REASONED; reload, reload-advisory): Confirm local backend health, then curl the container address from a second host with a bridge-subnet route before and after sudo firewall-cmd --reload. Without masking controls, affected hosts change from blocked to connectable; fixed hosts stay blocked. Any TCP connection, including HTTP 401/403, shows reachability; failure alone does not prove patching. Check route, target, IPv6, restricted host publications and continued proxy reachability. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Containers are where accidental exposure happens most. Two Docker behaviours cause it: @@ -7283,93 +8543,356 @@ REASONED: this firewalld reload check follows the Impact section of GHSA-x4rx-4g - Tailscale Funnel public entry (rolling reference reused from `tailscale.md`; not rechecked here): https://tailscale.com/kb/1223/funnel - OpenSSL documentation index (rolling reference reused from `self-signed.md`; not rechecked here): https://docs.openssl.org/ -====================================================================== -==> kubernetes.md -====================================================================== - -# Kubernetes: Gateway API TLS and authentication +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/docker.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + **Version basis** -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. -Metadata reviewed 2026-09-27; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- kubernetes: Kubernetes documentation (unknown) -- kubelet: Kubernetes kubelet/kubeadm source (v1.37.1) -- envoy: Envoy Gateway (v1.9.1) -- gateway: Gateway API documentation (unknown) -- cert-manager: cert-manager Gateway support minimum (1.15) -- apache: Apache htpasswd documentation (unknown) -- authelia: Authelia documentation (unknown) -- eks: Amazon EKS documentation (unknown) -- eks-ami: Amazon EKS AMI bootstrap (6caf8311a3c6a8da71ac7e5e83f9c2e06287039a) -- gke: GKE documentation (unknown) -- gke-min: GKE new-cluster read-only-port boundary (1.32) -- aks: AKS documentation (unknown) -- curl: curl documentation (unknown) -- nmap: Nmap documentation (unknown) -- traefik: Traefik documentation (unknown) -- cilium: Cilium documentation (unknown) -- envoy-rolling: Envoy Gateway documentation (rolling) (unknown) +- boundary: Docker Engine minimum boundary (28.0) +- moby-unit: Moby Docker service unit (v28.3.3) +- reload: Docker firewalld reload fix (28.3.3) +- docker-build: Docker build and image history documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- engine: Docker Engine networking (unknown) +- engine-pinned: Docker Engine networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- compose: Compose networking (unknown) +- docker-workload: Docker workload documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- reload-advisory: Docker firewalld reload advisory (unknown) +- compose-pinned: Docker Compose networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- systemd-unit: systemd unit ordering (v262) +- git-ignore: Git ignore rules (v2.49.0) +- postgresql: PostgreSQL (17) +- caddy-image: Caddy image (fba2853501d36e8a72f946ac8cb7ff64d07e48f2) +- caddy: Caddy (v2.11.4) +- nginx-auth: nginx authentication source (release-1.26.3) +- traefik-auth: Traefik authentication source (v3.5.0) +- cloudflare-access: Cloudflare Access documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- authelia-proxy: Authelia proxy integration (v4.39.28) +- aws-firewall: AWS security group rules (unknown) +- gcp-firewall: Google Cloud VPC firewall rules (unknown) +- azure-firewall: Azure network security group rules (unknown) +- cloudflared-image: cloudflared container image (2026.9.3) +- cloudflare-tunnel: Cloudflare Tunnel documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- tailscale-entry: Tailscale entry documentation (unknown) +- self-signed-entry: OpenSSL documentation (rolling) (unknown) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- ingress-migration (REASONED; kubernetes): ingress-nginx retired in March 2026 with no subsequent updates; detect its labeled pods and migrate. Gateway API is recommended and Ingress API is frozen. -- gateway-install (REASONED; envoy, gateway, envoy-rolling): Install Envoy Gateway chart v1.9.1 and its default Gateway CRDs; separately create GatewayClass eg with the documented controllerName and confirm Accepted=True. -- gateway-alternatives (REASONED; traefik, cilium): Traefik uses providers.kubernetesGateway; Cilium uses gatewayAPI.enabled=true and requires kube-proxy replacement. -- gateway-tls (REASONED; gateway, envoy-rolling): Gateway HTTPS listener 443 terminates TLS using app-tls for app.example.com; HTTP 80 is for redirect and ACME challenges. -- gateway-route (REASONED; gateway): HTTPRoute app attaches only to the HTTPS listener via sectionName and forwards to the ClusterIP app Service on port 80. -- gateway-redirect (REASONED; gateway, envoy-rolling): The HTTP route uses RequestRedirect with scheme https and statusCode 301. -- cert-manager-enable (REASONED; cert-manager): From cert-manager 1.15, enable config.gatewayAPI.enabled; install Gateway CRDs before startup or restart cert-manager afterwards. -- cert-manager-issue (REASONED; cert-manager): Gateway issuer annotations produce Certificates per HTTPS-listener Secret using listener hostnames; ACME gatewayHTTPRoute parentRefs selects the challenge Gateway. -- basic-auth-policy (REASONED; gateway, envoy-rolling): Gateway API has no standard auth filter; Envoy SecurityPolicy attaches Basic auth to Gateway/HTTPRoute/GRPCRoute using an htpasswd Secret. -- basic-auth-hash (REASONED; apache, envoy-rolling): Envoy supports SHA hashes only; htpasswd -s is weak SHA-1, so use long random passwords over TLS and keep application login. -- basic-auth-input (REASONED; apache, envoy-rolling): htpasswd -i reads stdin, unlike argv-exposing -b; confirm twice, create with -cis, verify with -vi and only then create the Kubernetes Secret. -- oidc (REASONED; envoy-rolling): Envoy SecurityPolicy oidc uses issuer, clientID, clientSecret and redirectURL; enforce MFA at the identity provider. -- external-auth (REASONED; authelia, envoy-rolling): Authelia is an authorization endpoint, not a traffic proxy; Envoy extAuth.http targets its Service and /api/authz/ext-authz/. -- workload-exposure (REASONED; envoy-rolling, kubernetes): Expose workloads only through the Gateway LoadBalancer; keep databases on ClusterIP without routes and enforce pod access with NetworkPolicies plus database TLS/auth. -- secrets (REASONED; kubernetes): Store credentials in Secrets or an external operator, not committed ConfigMaps or environment literals; kubeconfigs containing credentials also need secret handling. -- eks-endpoint (REASONED; eks): EKS API defaults public; restrict public CIDRs or use private access. Private-only endpoints can resolve publicly to private VPC addresses. -- gke-endpoints (REASONED; gke): GKE authorized networks constrain IP-based access, not its separately IAM-gated DNS endpoint; inspect both and disable unused DNS access. -- aks-endpoint (REASONED; aks): Restrict AKS API access with authorized IP ranges; workload Gateway protection does not secure the control plane. -- kubeconfig (REASONED; kubernetes): Embedded tokens or client keys confer their RBAC access without another factor; exec-plugin configs may be only pointers, but args/env can themselves hold credentials. -- api-port (REASONED; kubernetes, eks): API server commonly uses 6443, but providers may use 443; use the complete kubeconfig endpoint and its actual port. -- etcd-ports (REASONED; kubernetes): etcd client/peer defaults are 2379/2380; require certificates, restrict holders and private access, and retain loopback listeners used by the local API server. -- etcd-storage (REASONED; kubernetes): API resources default to plaintext storage in etcd; valid client certificates are the boundary and etcd access is equivalent to cluster root. -- scheduler-port (REASONED; kubernetes): Scheduler default 10259 must remain restricted to cluster clients. -- controller-port (REASONED; kubernetes): Controller-manager default 10257 must remain restricted to cluster clients. -- proxy-port (REASONED; kubernetes): Worker kube-proxy health default 10256 needs separate node exposure checks. -- nodeports (REASONED; kubernetes): NodePort defaults span 30000 to 32767 TCP/UDP; Service listings do not actively probe that range. -- kubelet-legacy-anonymous (REASONED; kubelet): Legacy kubelet flags default anonymous-auth=true; explicitly disable it where flags control authentication. -- kubelet-legacy-webhook (REASONED; kubelet): Legacy authentication-token-webhook defaults false; explicitly enable it where flags configure authentication. -- kubelet-legacy-authz (REASONED; kubelet, kubernetes): Legacy authorization-mode defaults AlwaysAllow; set Webhook where flags configure authorization. -- kubelet-file-anonymous (REASONED; kubelet): v1beta1 KubeletConfiguration instead defaults authentication.anonymous.enabled=false. -- kubelet-file-webhook (REASONED; kubelet): v1beta1 KubeletConfiguration defaults authentication.webhook.enabled=true. -- kubelet-file-authz (REASONED; kubelet): v1beta1 KubeletConfiguration defaults authorization.mode=Webhook. -- kubelet-precedence (REASONED; kubelet): Explicit flags override --config; --config-dir drop-ins override defaults and the file. Inspect all effective settings together. -- kubeadm (REASONED; kubelet): kubeadm sets anonymous false, token webhook true, Webhook authorization and readOnlyPort 0; user overrides are preserved with warnings. -- eks-bootstrap (REASONED; eks-ami): The pinned EKS AMI bootstrap writes Anonymous.Enabled=false, Mode=Webhook and ReadOnlyPort=0; do not generalize to all managed nodes. -- kubelet-bind (REASONED; kubelet): Secured kubelet defaults to address 0.0.0.0 on 10250; wildcard help includes both families. Bind private addresses and restrict authorized callers. -- kubelet-exec (REASONED; kubelet): 10250 debugging handlers can execute in containers; authentication does not justify public reachability. -- readonly-default (REASONED; kubelet): Legacy read-only port defaults 10255, while v1beta1 readOnlyPort defaults 0; explicitly disable it and remove enabling overrides. -- readonly-auth (REASONED; kubelet): Read-only server uses plain HTTP with no authentication or authorization filter; its caller-supplied bind wiring was outside the inspected source subset. -- readonly-paths (REASONED; kubelet): Read-only handlers expose /pods, /stats/summary, metrics variants and health; v1.37.1 has no /spec handler. Securing 10250 does not secure 10255. -- gke-readonly (REASONED; gke-min): GKE disables the read-only port by default only for new clusters running 1.32+; inspect effective configuration on older or upgraded clusters. -- verify-services (REASONED; kubernetes, envoy-rolling, cert-manager): Only the Gateway should have NodePort/LoadBalancer exposure; verify its public address/DNS and Certificate Ready=True. -- verify-redirect (REASONED; gateway): HTTP should return 301 to the HTTPS application URL. -- verify-basic (REASONED; curl, envoy-rolling): Anonymous Basic-protected access should return 401; require SecurityPolicy Accepted=True and a valid credential reaching the app without the gateway Basic challenge. -- verify-api-target (REASONED; kubernetes): Use the whole kubeconfig endpoint, preserving IPv6 and inferring a schemeless endpoint from both cluster and user TLS settings, not a hardcoded 6443. -- verify-proxies (REASONED; curl): Disable curl config and configured proxies for direct probes; a transparent TLS middlebox can still answer for an unreachable target. -- verify-api-tcp (REASONED; kubernetes, eks): Outside allowed ranges, nc success proves something accepted TCP; corroborate failed probes with provider ranges and multiple vantage points. -- verify-api-http (REASONED; curl, eks): Any HTTP status with curl exit 0 means answered; exit 60 means a TLS peer answered, 7 failed connect, 28 is ambiguous timeout and 6 is DNS failure, not privacy proof. -- verify-node-scan (REASONED; kubernetes, nmap): Scan control-plane hosts first and every public node address in both families; workers alone miss API/etcd and separate etcd hosts are absent from kubectl nodes. -- verify-kubelet (REASONED; kubelet): Untrusted clients should reach neither kubelet port after isolation, while authorized clients still reach 10250; readOnlyPort=0 should leave no 10255 listener even for authorized clients. +- publish (REASONED; engine): Omitting the host address publishes 3000:3000 on 0.0.0.0 and [::]. +- host-firewall (REASONED; engine): Docker programs firewall rules directly; UFW or firewalld blocking a host port does not establish published-port isolation. +- private-network (REASONED; compose, postgresql): Omit app/database ports; Compose peers use service names. The example uses postgres:17 at db:5432. +- loopback (REASONED; engine, boundary, reload, reload-advisory): Publish local access as 127.0.0.1:3000:3000; check the running server version. Engine 28.0 is the minimum boundary, with 28.3.3 or later required where firewalld is used. +- old-loopback (REASONED; boundary): Before 28.0, same-L2 neighbours could reach loopback publications under the default bridge configuration. +- old-host-bind (REASONED; boundary): Before 28.0, remote hosts could reach published container ports despite the host-IP binding. +- old-unpublished (REASONED; boundary): Before 28.0, direct routing could reach unpublished container ports; 28.0 fixed the stated default-bridge exposures. +- upstream (REASONED; engine, aws-firewall, gcp-firewall, azure-firewall): Use an upstream security group or network ACL for routable publications and boot-time protection. AWS security groups, Google Cloud VPC firewall rules and Azure NSGs have rolling sources; network-ACL configuration and boot-time independence remain this guide's assessment. +- docker-user (REASONED; engine): On the iptables backend, DOCKER-USER precedes Docker accepts; the nftables backend has no such chain. Match NEW traffic from outside the allowed subnet. +- dnat (REASONED; engine): DOCKER-USER sees post-DNAT container destinations; conntrack original destination port and ORIGINAL direction scope a published service. +- ipv6 (REASONED; engine, engine-pinned): As this guide's reasoning, native IPv6 forwarding needs ip6tables; Docker documents IPv6-to-IPv4 userland-proxy mapping when no host IP is given, the bridge is IPv4-only and `--userland-proxy=true` (the default), and places DOCKER-USER in FORWARD. As this guide's reasoning, the proxy path terminates on host INPUT and bypasses DOCKER-USER. +- proxy-path (REASONED; engine, engine-pinned): Close the userland-proxy path with an explicit publish address, disabled userland proxy or IPv6 INPUT restrictions; test both address families and sources. Docker documents the explicit-address option and conditional userland-proxy mapping; the INPUT restriction and probes are this guide's reasoning and advice. +- persistence (REASONED; engine, engine-pinned, moby-unit, systemd-unit): As this guide's advice, persist only DOCKER-USER rules after Docker startup, avoiding blanket dynamic-chain saves. Moby identifies docker.service and systemd documents After= ordering; as this guide's assessment, rule non-persistence, the boot-time gap, what iptables-persistent captures and its potential to break container networking have no direct source here. +- tls (REASONED; compose, caddy-image, caddy): Example caddy:2 publishes 80:80 and 443:443, mounts Caddyfile read-only and persists data/config, proxying app:3000 with automatic certificates. +- alternate-entry (REASONED; compose, cloudflared-image, cloudflare-tunnel, compose-pinned, tailscale-entry, self-signed-entry): As this guide's advice, hosts with no inbound ports and a domain they can put on Cloudflare use a cloudflared container, while hosts with no domain at all use Tailscale or internal self-signed TLS. Cloudflare documents a tunnel daemon that needs no firewall openings, a container image, an HTTPS origin service URL and a domain prerequisite; Docker documents Compose service-name lookup. The http://app:3000 origin target on a shared Compose network is this guide's inference, not shown literally by the fetched Cloudflare lines. The tunnel-token setup is not shown here; the Tailscale and OpenSSL references are rolling, and the self-signed fallback remains this guide's advice. +- auth (REASONED; compose, caddy, nginx-auth, traefik-auth, cloudflare-access, authelia-proxy): As this guide's assessment, proxy authentication and human MFA supplement application login. Caddy and nginx basic authentication reject missing credentials; nginx auth_request returns 401/403; Traefik basicAuth calls RequireAuth and forwardAuth passes through non-2xx; Cloudflare Access policies can require MFA, and Authelia can sit behind the proxy. Require MFA in the governing policy or portal; as this guide's assessment, basic auth alone is single-factor. The Caddy forward_auth rejection status, exact Traefik basicAuth status and Cloudflare Access anonymous status are not established by these fetched lines. +- secrets (REASONED; compose, docker-build): Use runtime environment files or Compose secrets, exclude .env from Git, and never put secrets in Dockerfile ENV/ARG; use temporary build secret mounts. Docker build and history behaviour is sourced; as this guide's assessment, the cited history reference does not establish that every argument value is shown; as this guide's assessment, never bake secrets into images and do not copy or print build-time secrets into the image. No direct source for the runtime delivery methods or the .env exclusion from Git is listed. +- workload (REASONED; compose, git-ignore, docker-workload): Keep .env in .gitignore, which ignores matching untracked files but not tracked files. Use a non-root Dockerfile USER; as this guide's assessment, a compromised app then is not root in the container. Drop capabilities, use no-new-privileges and a read-only root filesystem, and never mount the Docker socket. Docker sources establish the option names, that only trusted users should control the daemon, and that Unix permissions can limit access to its control socket; the linked guide's hardening consequences, including the mounted-socket path, remain this guide's assessment. +- database (REASONED; compose): Database TLS and authentication remain necessary for connections from outside the Compose network; database vendor sources are in linked guides. +- verify-publish (REASONED; compose, engine): Compose ps is project-scoped; inventory every host container and TCP/UDP socket, including IPv6. Socket visibility alone does not prove reachability. +- verify-redirect (REASONED; compose, caddy): Verify that HTTP redirects to HTTPS: Caddy v2.11.4 configures an automatic HTTPS redirect; as this guide's configuration-dependent expectation, nginx and Traefik require a configured redirect. This is an expectation, not an observed deployment result. +- verify-auth (REASONED; compose, caddy, nginx-auth, traefik-auth, authelia-proxy, cloudflare-access): Verify unauthenticated HTTPS /api rejection with printed status and redirect location; the probe itself fails on any 2xx. A 3xx passes only when the location is the gate's own login portal, never the application's own login; verify authorized success separately. Caddy basic auth and nginx auth_basic expect 401; Caddy forward_auth admits auth-service 2xx, but its rejection status is not established by the fetched lines. nginx auth_request returns 401 or 403, while the cited Authelia nginx configuration turns 401 into 302 to its portal. Traefik forwardAuth passes through a rejecting auth service's status, including a login redirect; Traefik basicAuth's 401 is this guide's expectation because the fetched code calls RequireAuth, whose response these lines do not show. Cloudflare Access's anonymous 302 login redirect is this guide's assessment per cloudflare.md, not established by the fetched Cloudflare lines. +- verify-isolation (REASONED; engine): Probe each restricted host address and publication from allowed/disallowed sources in both families; backend isolation must coexist with proxy reachability. +- firewalld-reload (REASONED; reload, reload-advisory): Engine 28.2.x and 28.3.0 through 28.3.2 fail to restore container-address filtering after firewalld reload; 28.3.3 fixes CVE-2025-54388 (GHSA-x4rx-4gw3-53p4). +- firewalld-scope (REASONED; reload, reload-advisory): The regression affects Linux Engine in the host network namespace with firewalld: routed remote hosts can reach published container ports, including loopback publications; unpublished ports stay filtered. Rootless Mode and Docker Desktop are unaffected. +- verify-firewalld-reload (REASONED; reload, reload-advisory): Confirm local backend health, then curl the container address from a second host with a bridge-subnet route before and after sudo firewall-cmd --reload. Without masking controls, affected hosts change from blocked to connectable; fixed hosts stay blocked. Any TCP connection, including HTTP 401/403, shows reachability; failure alone does not prove patching. Check route, target, IPv6, restricted host publications and continued proxy reachability. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "0a7bc9ec3709af315d7dab37b1fe1b6343f4b7b6425ccde1b041955813382cf5", + "components": { + "boundary": { + "name": "Docker Engine minimum boundary", + "basis": "28.0", + "sources": { + "s50eb099eac95": "https://docs.docker.com/engine/release-notes/28/" + } + }, + "moby-unit": { + "name": "Moby Docker service unit", + "basis": "v28.3.3", + "sources": { + "s6aa0d2fb34cf": "https://github.com/moby/moby/blob/v28.3.3/contrib/init/systemd/docker.service#L1-L9" + } + }, + "reload": { + "name": "Docker firewalld reload fix", + "basis": "28.3.3", + "sources": { + "s567a65842b29": "https://docs.docker.com/engine/release-notes/28/#2833", + "seaf4197aafaa": "https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go" + } + }, + "docker-build": { + "name": "Docker build and image history documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "sd8afa286c173": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/build/building/variables.md#L17-L52", + "s07e00829f121": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/build/building/secrets.md#L13-L22", + "s6283b172a5c6": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_image_history.yaml#L1-L118" + } + }, + "engine": { + "name": "Docker Engine networking", + "basis": "unknown", + "sources": { + "s351180c6678f": "https://docs.docker.com/engine/network/packet-filtering-firewalls/", + "s2fecb6db5480": "https://docs.docker.com/engine/network/firewall-iptables/", + "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" + } + }, + "engine-pinned": { + "name": "Docker Engine networking documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "sd22373a822c3": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/firewall-iptables.md#L8-L15", + "s024cfadb4bb8": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/firewall-iptables.md#L17-L42", + "s1c82ff316a7e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L60-L62", + "sb3dacfbaf7e1": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L33-L38", + "sc244607b20e4": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15" + } + }, + "compose": { + "name": "Compose networking", + "basis": "unknown", + "sources": { + "sae565a19136c": "https://docs.docker.com/compose/how-tos/networking/" + } + }, + "docker-workload": { + "name": "Docker workload documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "s2fa3b3cd70e6": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/_vendor/github.com/moby/buildkit/frontend/dockerfile/docs/reference.md#L2448-L2463", + "s8000f273883e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L224-L233", + "sb94d21792cd4": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L1892-L1894", + "s28ce092b2589": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/reference/compose-file/services.md#L2008-L2021", + "s738280652c95": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L2495-L2502", + "secebea1a69ab": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/_index.md#L84-L112" + } + }, + "reload-advisory": { + "name": "Docker firewalld reload advisory", + "basis": "unknown", + "sources": { + "s424f3c4f8285": "https://osv.dev/vulnerability/GHSA-x4rx-4gw3-53p4" + } + }, + "compose-pinned": { + "name": "Docker Compose networking documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "seb7ab83ee20e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/compose/how-tos/networking.md#L13-L17" + } + }, + "systemd-unit": { + "name": "systemd unit ordering", + "basis": "v262", + "sources": { + "s71dce3a476cd": "https://github.com/systemd/systemd/blob/v262/man/systemd.unit.xml#L826-L838" + } + }, + "git-ignore": { + "name": "Git ignore rules", + "basis": "v2.49.0", + "sources": { + "s5f377f0fdbd8": "https://github.com/git/git/blob/v2.49.0/Documentation/gitignore.adoc#L15-L18" + } + }, + "postgresql": { + "name": "PostgreSQL", + "basis": "17", + "sources": { + "s44c2e898e585": "https://www.postgresql.org/docs/17/runtime-config-connection.html" + } + }, + "caddy-image": { + "name": "Caddy image", + "basis": "fba2853501d36e8a72f946ac8cb7ff64d07e48f2", + "sources": { + "sbbb7746f7545": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L44-L45", + "sb126b380b9cf": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L56-L59", + "seea3a2adc92a": "https://github.com/caddyserver/caddy-docker/blob/fba2853501d36e8a72f946ac8cb7ff64d07e48f2/2.11/alpine/Dockerfile#L63" + } + }, + "caddy": { + "name": "Caddy", + "basis": "v2.11.4", + "sources": { + "s5520ee906a7f": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L62-L67", + "sfa5c7dc047dc": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L60", + "s32fb97ff942b": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/caddyauth/caddyauth.go#L84-L120", + "sd698097c71f4": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/caddyauth/basicauth.go#L142-L163", + "s87dc43d9819d": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go#L174-L194", + "sd267d297d661": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L234-L240", + "s32a5119bce5b": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/autohttps.go#L544-L575" + } + }, + "nginx-auth": { + "name": "nginx authentication source", + "basis": "release-1.26.3", + "sources": { + "s44cecab17a23": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_basic_module.c#L122-L129", + "s91b029555086": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_basic_module.c#L317-L348", + "s3df6e64cd6a3": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_auth_request_module.c#L135-L182" + } + }, + "traefik-auth": { + "name": "Traefik authentication source", + "basis": "v3.5.0", + "sources": { + "s5f9a13d720c8": "https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/basic_auth.go#L67-L99", + "sfe588a6dd6a3": "https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/forward.go#L93-L99", + "sbc20b015d928": "https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/forward.go#L233-L262" + } + }, + "cloudflare-access": { + "name": "Cloudflare Access documentation", + "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "s6f6104362214": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L17-L85", + "s7cafe1b17afd": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app.mdx#L40-L54" + } + }, + "authelia-proxy": { + "name": "Authelia proxy integration", + "basis": "v4.39.28", + "sources": { + "s5ca02c783843": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/support.md#L24-L32", + "s31a83fce2b6e": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/integration/proxies/nginx.md#L537-L541" + } + }, + "aws-firewall": { + "name": "AWS security group rules", + "basis": "unknown", + "sources": { + "s5594f61a25d6": "https://docs.aws.amazon.com/vpc/latest/userguide/security-group-rules.html" + } + }, + "gcp-firewall": { + "name": "Google Cloud VPC firewall rules", + "basis": "unknown", + "sources": { + "s1c06c98a99c9": "https://docs.cloud.google.com/firewall/docs/using-firewalls" + } + }, + "azure-firewall": { + "name": "Azure network security group rules", + "basis": "unknown", + "sources": { + "scde5b083d938": "https://learn.microsoft.com/en-us/rest/api/virtualnetwork/network-security-groups/get" + } + }, + "cloudflared-image": { + "name": "cloudflared container image", + "basis": "2026.9.3", + "sources": { + "se79f4a693e7a": "https://github.com/cloudflare/cloudflared/blob/2026.9.3/README.md#L1-L5", + "s8b0cac0c9e3c": "https://github.com/cloudflare/cloudflared/blob/2026.9.3/README.md#L26-L33", + "s5c722efb03b3": "https://github.com/cloudflare/cloudflared/blob/2026.9.3/Dockerfile#L40-L42" + } + }, + "cloudflare-tunnel": { + "name": "Cloudflare Tunnel documentation", + "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "s253af0f0386c": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/partials/cloudflare-one/tunnel/origin-parameters.mdx#L31-L49", + "sbd8e87e6c172": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel.mdx#L27-L37" + } + }, + "tailscale-entry": { + "name": "Tailscale entry documentation", + "basis": "unknown", + "sources": { + "s8a8000573b13": "https://tailscale.com/kb/1242/tailscale-serve", + "sbe30f8121245": "https://tailscale.com/kb/1223/funnel" + } + }, + "self-signed-entry": { + "name": "OpenSSL documentation (rolling)", + "basis": "unknown", + "sources": { + "s211b1e6e4aa4": "https://docs.openssl.org/" + } + } + }, + "claims": { + "publish": {"text": "Omitting the host address publishes 3000:3000 on 0.0.0.0 and [::].", "components": ["engine"], "sources": ["engine:s1e53417c513d"], "status": "REASONED"}, + "host-firewall": {"text": "Docker programs firewall rules directly; UFW or firewalld blocking a host port does not establish published-port isolation.", "components": ["engine"], "sources": ["engine:s351180c6678f"], "status": "REASONED"}, + "private-network": {"text": "Omit app/database ports; Compose peers use service names. The example uses postgres:17 at db:5432.", "components": ["compose", "postgresql"], "sources": ["compose:sae565a19136c", "postgresql:s44c2e898e585"], "status": "REASONED"}, + "loopback": {"text": "Publish local access as 127.0.0.1:3000:3000; check the running server version. Engine 28.0 is the minimum boundary, with 28.3.3 or later required where firewalld is used.", "components": ["engine", "boundary", "reload", "reload-advisory"], "sources": ["engine:s1e53417c513d", "boundary:s50eb099eac95", "reload:s567a65842b29", "reload-advisory:s424f3c4f8285"], "status": "REASONED"}, + "old-loopback": {"text": "Before 28.0, same-L2 neighbours could reach loopback publications under the default bridge configuration.", "components": ["boundary"], "sources": ["boundary:s50eb099eac95"], "status": "REASONED"}, + "old-host-bind": {"text": "Before 28.0, remote hosts could reach published container ports despite the host-IP binding.", "components": ["boundary"], "sources": ["boundary:s50eb099eac95"], "status": "REASONED"}, + "old-unpublished": {"text": "Before 28.0, direct routing could reach unpublished container ports; 28.0 fixed the stated default-bridge exposures.", "components": ["boundary"], "sources": ["boundary:s50eb099eac95"], "status": "REASONED"}, + "upstream": {"text": "Use an upstream security group or network ACL for routable publications and boot-time protection. AWS security groups, Google Cloud VPC firewall rules and Azure NSGs have rolling sources; network-ACL configuration and boot-time independence remain this guide's assessment.", "components": ["engine", "aws-firewall", "gcp-firewall", "azure-firewall"], "sources": ["engine:s351180c6678f", "aws-firewall:s5594f61a25d6", "gcp-firewall:s1c06c98a99c9", "azure-firewall:scde5b083d938"], "status": "REASONED"}, + "docker-user": {"text": "On the iptables backend, DOCKER-USER precedes Docker accepts; the nftables backend has no such chain. Match NEW traffic from outside the allowed subnet.", "components": ["engine"], "sources": ["engine:s351180c6678f", "engine:s2fecb6db5480"], "status": "REASONED"}, + "dnat": {"text": "DOCKER-USER sees post-DNAT container destinations; conntrack original destination port and ORIGINAL direction scope a published service.", "components": ["engine"], "sources": ["engine:s2fecb6db5480"], "status": "REASONED"}, + "ipv6": {"text": "As this guide's reasoning, native IPv6 forwarding needs ip6tables; Docker documents IPv6-to-IPv4 userland-proxy mapping when no host IP is given, the bridge is IPv4-only and `--userland-proxy=true` (the default), and places DOCKER-USER in FORWARD. As this guide's reasoning, the proxy path terminates on host INPUT and bypasses DOCKER-USER.", "components": ["engine", "engine-pinned"], "sources": ["engine:s2fecb6db5480", "engine:s1e53417c513d", "engine-pinned:sd22373a822c3", "engine-pinned:s024cfadb4bb8", "engine-pinned:s1c82ff316a7e"], "status": "REASONED"}, + "proxy-path": {"text": "Close the userland-proxy path with an explicit publish address, disabled userland proxy or IPv6 INPUT restrictions; test both address families and sources. Docker documents the explicit-address option and conditional userland-proxy mapping; the INPUT restriction and probes are this guide's reasoning and advice.", "components": ["engine", "engine-pinned"], "sources": ["engine:s1e53417c513d", "engine:s2fecb6db5480", "engine-pinned:sb3dacfbaf7e1", "engine-pinned:s1c82ff316a7e", "engine-pinned:s024cfadb4bb8"], "status": "REASONED"}, + "persistence": {"text": "As this guide's advice, persist only DOCKER-USER rules after Docker startup, avoiding blanket dynamic-chain saves. Moby identifies docker.service and systemd documents After= ordering; as this guide's assessment, rule non-persistence, the boot-time gap, what iptables-persistent captures and its potential to break container networking have no direct source here.", "components": ["engine", "engine-pinned", "moby-unit", "systemd-unit"], "sources": ["engine:s351180c6678f", "engine:s2fecb6db5480", "engine-pinned:sc244607b20e4", "engine-pinned:s024cfadb4bb8", "moby-unit:s6aa0d2fb34cf", "systemd-unit:s71dce3a476cd"], "status": "REASONED"}, + "tls": {"text": "Example caddy:2 publishes 80:80 and 443:443, mounts Caddyfile read-only and persists data/config, proxying app:3000 with automatic certificates.", "components": ["compose", "caddy-image", "caddy"], "sources": ["compose:sae565a19136c", "caddy-image:sbbb7746f7545", "caddy-image:sb126b380b9cf", "caddy-image:seea3a2adc92a", "caddy:s5520ee906a7f", "caddy:sfa5c7dc047dc"], "status": "REASONED"}, + "alternate-entry": {"text": "As this guide's advice, hosts with no inbound ports and a domain they can put on Cloudflare use a cloudflared container, while hosts with no domain at all use Tailscale or internal self-signed TLS. Cloudflare documents a tunnel daemon that needs no firewall openings, a container image, an HTTPS origin service URL and a domain prerequisite; Docker documents Compose service-name lookup. The http://app:3000 origin target on a shared Compose network is this guide's inference, not shown literally by the fetched Cloudflare lines. The tunnel-token setup is not shown here; the Tailscale and OpenSSL references are rolling, and the self-signed fallback remains this guide's advice.", "components": ["compose", "cloudflared-image", "cloudflare-tunnel", "compose-pinned", "tailscale-entry", "self-signed-entry"], "sources": ["compose:sae565a19136c", "cloudflared-image:se79f4a693e7a", "cloudflared-image:s8b0cac0c9e3c", "cloudflared-image:s5c722efb03b3", "cloudflare-tunnel:s253af0f0386c", "cloudflare-tunnel:sbd8e87e6c172", "compose-pinned:seb7ab83ee20e", "tailscale-entry:s8a8000573b13", "tailscale-entry:sbe30f8121245", "self-signed-entry:s211b1e6e4aa4"], "status": "REASONED"}, + "auth": {"text": "As this guide's assessment, proxy authentication and human MFA supplement application login. Caddy and nginx basic authentication reject missing credentials; nginx auth_request returns 401/403; Traefik basicAuth calls RequireAuth and forwardAuth passes through non-2xx; Cloudflare Access policies can require MFA, and Authelia can sit behind the proxy. Require MFA in the governing policy or portal; as this guide's assessment, basic auth alone is single-factor. The Caddy forward_auth rejection status, exact Traefik basicAuth status and Cloudflare Access anonymous status are not established by these fetched lines.", "components": ["compose", "caddy", "nginx-auth", "traefik-auth", "cloudflare-access", "authelia-proxy"], "sources": ["compose:sae565a19136c", "caddy:s32fb97ff942b", "caddy:sd698097c71f4", "caddy:s87dc43d9819d", "nginx-auth:s44cecab17a23", "nginx-auth:s91b029555086", "nginx-auth:s3df6e64cd6a3", "traefik-auth:s5f9a13d720c8", "traefik-auth:sfe588a6dd6a3", "traefik-auth:sbc20b015d928", "cloudflare-access:s6f6104362214", "cloudflare-access:s7cafe1b17afd", "authelia-proxy:s5ca02c783843"], "status": "REASONED"}, + "secrets": {"text": "Use runtime environment files or Compose secrets, exclude .env from Git, and never put secrets in Dockerfile ENV/ARG; use temporary build secret mounts. Docker build and history behaviour is sourced; as this guide's assessment, the cited history reference does not establish that every argument value is shown; as this guide's assessment, never bake secrets into images and do not copy or print build-time secrets into the image. No direct source for the runtime delivery methods or the .env exclusion from Git is listed.", "components": ["compose", "docker-build"], "sources": ["compose:sae565a19136c", "docker-build:sd8afa286c173", "docker-build:s07e00829f121", "docker-build:s6283b172a5c6"], "status": "REASONED"}, + "workload": {"text": "Keep .env in .gitignore, which ignores matching untracked files but not tracked files. Use a non-root Dockerfile USER; as this guide's assessment, a compromised app then is not root in the container. Drop capabilities, use no-new-privileges and a read-only root filesystem, and never mount the Docker socket. Docker sources establish the option names, that only trusted users should control the daemon, and that Unix permissions can limit access to its control socket; the linked guide's hardening consequences, including the mounted-socket path, remain this guide's assessment.", "components": ["compose", "git-ignore", "docker-workload"], "sources": ["compose:sae565a19136c", "git-ignore:s5f377f0fdbd8", "docker-workload:s2fa3b3cd70e6", "docker-workload:s8000f273883e", "docker-workload:sb94d21792cd4", "docker-workload:s28ce092b2589", "docker-workload:s738280652c95", "docker-workload:secebea1a69ab"], "status": "REASONED"}, + "database": {"text": "Database TLS and authentication remain necessary for connections from outside the Compose network; database vendor sources are in linked guides.", "components": ["compose"], "sources": ["compose:sae565a19136c"], "status": "REASONED"}, + "verify-publish": {"text": "Compose ps is project-scoped; inventory every host container and TCP/UDP socket, including IPv6. Socket visibility alone does not prove reachability.", "components": ["compose", "engine"], "sources": ["compose:sae565a19136c", "engine:s1e53417c513d"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "Verify that HTTP redirects to HTTPS: Caddy v2.11.4 configures an automatic HTTPS redirect; as this guide's configuration-dependent expectation, nginx and Traefik require a configured redirect. This is an expectation, not an observed deployment result.", "components": ["compose", "caddy"], "sources": ["compose:sae565a19136c", "caddy:sd267d297d661", "caddy:s32a5119bce5b"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "Verify unauthenticated HTTPS /api rejection with printed status and redirect location; the probe itself fails on any 2xx. A 3xx passes only when the location is the gate's own login portal, never the application's own login; verify authorized success separately. Caddy basic auth and nginx auth_basic expect 401; Caddy forward_auth admits auth-service 2xx, but its rejection status is not established by the fetched lines. nginx auth_request returns 401 or 403, while the cited Authelia nginx configuration turns 401 into 302 to its portal. Traefik forwardAuth passes through a rejecting auth service's status, including a login redirect; Traefik basicAuth's 401 is this guide's expectation because the fetched code calls RequireAuth, whose response these lines do not show. Cloudflare Access's anonymous 302 login redirect is this guide's assessment per cloudflare.md, not established by the fetched Cloudflare lines.", "components": ["compose", "caddy", "nginx-auth", "traefik-auth", "authelia-proxy", "cloudflare-access"], "sources": ["compose:sae565a19136c", "caddy:s32fb97ff942b", "caddy:sd698097c71f4", "caddy:s87dc43d9819d", "nginx-auth:s44cecab17a23", "nginx-auth:s91b029555086", "nginx-auth:s3df6e64cd6a3", "traefik-auth:s5f9a13d720c8", "traefik-auth:sfe588a6dd6a3", "traefik-auth:sbc20b015d928", "authelia-proxy:s31a83fce2b6e", "cloudflare-access:s7cafe1b17afd"], "status": "REASONED", "verify": [1]}, + "verify-isolation": {"text": "Probe each restricted host address and publication from allowed/disallowed sources in both families; backend isolation must coexist with proxy reachability.", "components": ["engine"], "sources": ["engine:s351180c6678f", "engine:s2fecb6db5480", "engine:s1e53417c513d"], "status": "REASONED", "verify": [1]}, + "firewalld-reload": {"text": "Engine 28.2.x and 28.3.0 through 28.3.2 fail to restore container-address filtering after firewalld reload; 28.3.3 fixes CVE-2025-54388 (GHSA-x4rx-4gw3-53p4).", "components": ["reload", "reload-advisory"], "sources": ["reload:s567a65842b29", "reload-advisory:s424f3c4f8285", "reload:seaf4197aafaa"], "status": "REASONED"}, + "firewalld-scope": {"text": "The regression affects Linux Engine in the host network namespace with firewalld: routed remote hosts can reach published container ports, including loopback publications; unpublished ports stay filtered. Rootless Mode and Docker Desktop are unaffected.", "components": ["reload", "reload-advisory"], "sources": ["reload:s567a65842b29", "reload-advisory:s424f3c4f8285"], "status": "REASONED"}, + "verify-firewalld-reload": {"text": "Confirm local backend health, then curl the container address from a second host with a bridge-subnet route before and after sudo firewall-cmd --reload. Without masking controls, affected hosts change from blocked to connectable; fixed hosts stay blocked. Any TCP connection, including HTTP 401/403, shows reachability; failure alone does not prove patching. Check route, target, IPv6, restricted host publications and continued proxy reachability.", "components": ["reload", "reload-advisory"], "sources": ["reload-advisory:s424f3c4f8285", "reload:seaf4197aafaa"], "status": "REASONED"} + } +} +```` +====================================================================== +==> kubernetes.md +====================================================================== + +# Kubernetes: Gateway API TLS and authentication + +Written against Kubernetes kubelet/kubeadm source v1.37.1, Envoy Gateway v1.9.1, cert-manager Gateway support minimum 1.15, and GKE new-cluster read-only-port boundary 1.32; 1 further source is pinned to a commit and 12 carry no recorded version. Metadata reviewed 2026-09-27; documentation checked September 2026. The guide records 50 claims, all reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + The cluster equivalents of this repository's rules: nothing reaches a workload except through the TLS-terminating entry point (a Gateway API `Gateway`), and no Service becomes public through a casual `type: LoadBalancer` or `NodePort`; the entry point's own Service is the only exception. **If you run ingress-nginx today, migrate.** Earlier versions of this guide built on ingress-nginx. The Kubernetes project retired it in March 2026: per the Kubernetes Steering and Security Response Committees, "there will be no more releases for bug fixes, security patches, or any updates of any kind after the project is retired", and "choosing to remain with Ingress NGINX after its retirement leaves you and your users vulnerable to attack" (as of September 2026; statement linked in Sources). Detect it with cluster-admin permissions: `kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx`. Any pod returned means migration is required; the `nginx.ingress.kubernetes.io/*` annotations die with the controller. Kubernetes recommends Gateway API over Ingress (whose API is now frozen) and links a migration guide from its Gateway API page. The rest of this guide is the Gateway API form of the old rules. @@ -7791,85 +9314,337 @@ allowed ranges out of the provider's own configuration rather than inferring the - cert-manager Gateway API usage (enabling support, annotations; since cert-manager 1.15, Gateway API support no longer requires a feature gate but still requires explicit enablement): https://cert-manager.io/docs/usage/gateway/ ; ACME HTTP-01 `gatewayHTTPRoute` solver: https://cert-manager.io/docs/configuration/acme/http01/ - Traefik Kubernetes Gateway API provider: https://doc.traefik.io/traefik/reference/install-configuration/providers/kubernetes/kubernetes-gateway/ ; Cilium Gateway API support: https://docs.cilium.io/en/stable/network/servicemesh/gateway-api/gateway-api/ -====================================================================== -==> gitops-controllers.md -====================================================================== - -# GitOps controllers: Argo CD and Flux +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/kubernetes.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + **Version basis** -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- argo: Argo CD v3.5.3 source (c9c369efcc5b2a0bd720803f8d14a1c3eaddf579) -- dex: Dex v2.45.1 source (v2.45.1) -- redis-image: docker-library-redis v8.2.3 source (bcbc692165ce2241e194035041c5b5e01d2e1a5f) -- redis-source: Redis 8.2.3 source (8.2.3) -- argo-docs: Argo CD documentation (unknown) -- curl: curl (curl-8_14_1) -- flux: Flux documentation (unknown) -- flux-oidc: Flux OIDC introduction (v2.9) -- python: Python documentation (unknown) +- kubelet: Kubernetes kubelet/kubeadm source (v1.37.1) +- kubernetes: Kubernetes documentation (unknown) +- gateway: Gateway API documentation (unknown) +- envoy: Envoy Gateway (v1.9.1) +- cert-manager: cert-manager Gateway support minimum (1.15) +- apache: Apache htpasswd documentation (unknown) +- authelia: Authelia documentation (unknown) +- eks: Amazon EKS documentation (unknown) +- eks-ami: Amazon EKS AMI bootstrap (6caf8311a3c6a8da71ac7e5e83f9c2e06287039a) +- gke: GKE documentation (unknown) +- gke-min: GKE new-cluster read-only-port boundary (1.32) +- aks: AKS documentation (unknown) +- traefik: Traefik documentation (unknown) +- cilium: Cilium documentation (unknown) +- envoy-rolling: Envoy Gateway documentation (rolling) (unknown) +- curl: curl documentation (unknown) +- nmap: Nmap documentation (unknown) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- git-boundary (REASONED; argo-docs, flux): Repository writers exercise reconciliation permissions; review platform changes, separate tenants and scope credentials. -- secret-storage (REASONED; argo-docs, flux): Protect bootstrap, registry, cluster, webhook and decryption credentials; commit only encrypted secrets and keep SOPS/age keys out of Git. -- egress (REASONED; argo-docs, flux): Restrict fetch, API, identity and notification egress; Argo repository allowlists do not constrain remote bases or Helm dependencies. -- argo-bootstrap (REASONED; argo-docs): Rotate the initial admin password before deleting argocd-initial-admin-secret; deletion alone does not rotate it. -- argo-sso (REASONED; argo-docs): The built-in admin is unrestricted; configure named SSO roles and IdP MFA, then set admin.enabled=false. -- argo-origin (REASONED; argo-docs, argo): Keep the ClusterIP origin private behind authenticated TLS and protect both REST/browser and CLI/gRPC routes. -- argo-server (REASONED; argo): Server defaults to 0.0.0.0:8080; the Service maps 80 and 443 to it. -- argo-tls (REASONED; argo, argo-docs): Server --insecure/server.insecure defaults false and disables backend TLS, not authentication; use only behind protected TLS termination. -- argo-client-tls (REASONED; argo-docs): argocd login --insecure skips client certificate verification; the server initially uses a self-signed certificate. -- argo-metrics (REASONED; argo): Server HTTP metrics on 8083 are unauthenticated and bind to --address, not --metrics-address. -- argo-profiler (REASONED; argo): Server and repo-server pprof paths return 401 unless the profiler file contains exactly true with no newline; enabled profiling has no caller authentication. -- argo-profiler-file (REASONED; argo): ARGOCD_ENABLE_PROFILER_FILE_PATH defaults to /home/argocd/params/profiler.enabled; the server mounts ConfigMap key server.profile.enabled there. -- repo-grpc (REASONED; argo): Repo-server gRPC binds 0.0.0.0:8081 with TLS by default; mTLS is skipped without the client CA, separate from the serving certificate. -- repo-http (REASONED; argo): Repo-server HTTP metrics and health bind 0.0.0.0:8084 without an authentication wrapper. -- repo-limits (REASONED; argo): Repo-server requires verified client certificates with a loaded client CA; missing or unstatable serving files trigger a session self-signed certificate, while an invalid present pair errors. No authorization interceptor appears; service method authorization is unverified. -- controller-metrics (REASONED; argo): Application-controller metrics and health use 8082; the command passes that port to the controller, but the fetched files do not establish the bind address. -- applicationset-webhook (REASONED; argo): ApplicationSet webhook binds :7000; its container and Service declare 7000. -- applicationset-metrics (REASONED; argo): ApplicationSet metrics bind :8080; its container and Service declare 8080. -- applicationset-health (REASONED; argo): ApplicationSet health probes bind :8081 without a container or Service port declaration; missing declarations do not close sockets. -- notifications (REASONED; argo): Notifications HTTP metrics bind 0.0.0.0:9001 without authentication; a metrics Service and TCP liveness probe exist without containerPort. -- dex-http (REASONED; argo): Dex starts only with nonempty generated configuration; HTTP defaults to TLS on 0.0.0.0:5556 using /tmp/tls.crt and /tmp/tls.key. -- dex-grpc (REASONED; argo, dex): Generated Dex gRPC uses 0.0.0.0:5557 without TLS or client-auth settings; Dex v2.45.1 source sets no transport credentials or authentication interceptor for this configuration, while API method authorization is unverified; this assumes the deployed ghcr.io/dexidp/dex:v2.45.1 tag was built from Dex v2.45.1. -- dex-telemetry (REASONED; argo): Dex telemetry uses HTTP on 0.0.0.0:5558; all three Dex ports have Services. -- redis-password (REASONED; argo, redis-image, redis-source): Redis receives --requirepass from argocd-redis/auth; secret-init generates and checks that Secret. The redis:8.2.3-alpine tag, taken as built from docker-library-redis v8.2.3, uses Redis wildcard bind defaults and patches protected mode off. -- redis-tls (REASONED; argo): Base manifests configure no Redis TLS and the Argo Redis client defaults --redis-use-tls=false; protect traffic, Secret and cache. -- component-policies (REASONED; argo): Base policies restrict repo gRPC, Redis and Dex callers by component; enforcement needs a supporting CNI. -- broad-policies (REASONED; argo): Server policy allows all ingress; other metrics and ApplicationSet webhook allowances admit every namespace. Narrow existing additive allowances and egress separately. -- argo-anonymous (REASONED; argo, argo-docs): users.anonymous.enabled defaults off; anonymous callers otherwise inherit policy.default. -- argo-disable-auth (REASONED; argo): server.disable.auth defaults false; inspect effective args and environment because anonymous-off does not compensate for disabled authentication. -- argo-default-role (REASONED; argo, argo-docs): Base RBAC ConfigMap omits policy.default; the inspected loader supplies an empty role to the enforcer, which skips its additive default-role check. Default-role grants are additive and cannot be revoked by a later subject rule; define scoped policy.csv roles and group mappings. Server-side wiring remains unverified. -- argo-webhook (REASONED; argo-docs): /api/webhook accepts unauthenticated refresh events without a shared secret; configure one to limit spoofed reconciliation and resource exhaustion. -- argo-projects (REASONED; argo-docs): Restrict AppProjects including the built-in default project; project constraints do not reduce a compromised controller's Kubernetes credentials. -- argo-cluster-rbac (REASONED; argo-docs): Reduce controller and registered-cluster argocd-manager write permissions to needed namespaces/resources while retaining required read access. -- flux-api (REASONED; flux): Flux has no standalone management login/API; Kubernetes authenticates its custom-resource operations, while separate dashboards need their own controls. -- flux-bootstrap (REASONED; flux): GitHub bootstrap --token-auth retains a PAT in flux-system; --token-auth=false creates a read-only SSH deploy key by default. Protect bootstrap input and Secrets. -- flux-writeback (REASONED; flux): Image automation needs Git write access, including --read-write-key=true for the bootstrap key; scope the repository and protect update branches and objects. -- flux-artifacts (REASONED; flux): Source-controller serves fetched artifacts over internal HTTP; enforced NetworkPolicy, not ClusterIP alone, isolates them. -- flux-receiver-port (REASONED; flux): Webhook receiver listens on 9292 behind Service port 80; external exposure needs a route and TLS termination. -- flux-receiver-types (REASONED; flux): generic validates nothing; generic-hmac, GitHub, Bitbucket and Nexus use HMAC, while gitlab compares X-Gitlab-Token. -- flux-oidc (REASONED; flux, flux-oidc): generic-oidc, introduced in Flux 2.9, rejects secretRef and validates bearer tokens; constrain issuer validations and audience, whose default is notification-controller. -- flux-rbac (REASONED; flux): Only kustomize-controller and helm-controller hold cluster-admin; other controllers still have meaningful permissions including Secret access. -- flux-lockdown (REASONED; flux): Use scoped reconciliation ServiceAccounts, --no-cross-namespace-refs and --no-remote-bases; --default-service-account fills an omission, not an explicit serviceAccountName. -- verify-argo (REASONED; argo-docs, argo, curl): Anonymous application listing should fail with 401 while a valid token lists the expected application; repeat at origin and CLI/gRPC and inspect effective RBAC. -- verify-bootstrap (REASONED; argo-docs): Absence of the bootstrap Secret proves removal of that material, not password rotation. -- verify-inventory (REASONED; argo, flux): Inventory Services, routes, host ports, policies and component sockets; node-local ss and policy objects alone do not establish cluster isolation. -- verify-repo-metrics (REASONED; argo): Base policy permits HTTP 200 metrics to monitoring and unrelated pods; narrowed policy should admit only monitoring, with a successful positive control. -- verify-server-metrics (REASONED; argo): Repeat on server 8083: only authorized monitoring should receive metrics after narrowing ingress; a failed control is inconclusive. -- verify-profiler (REASONED; argo): From an allowed pod, pprof cmdline should return 401 disabled, 200 for exact true, then 401 for true plus newline; keep metrics as control. -- verify-flux (REASONED; flux, python, curl): Identical generic-hmac bodies without and with X-Signature should be rejected and accepted with reconciliation; status is type/version dependent. The cited curl curl-8_14_1 lines show --data-binary posting data exactly as specified, preserving newlines and carriage returns that --data strips from @file input, reading @- from stdin and @filename from a file, and posting like --data's POST request. -- verify-receiver-status (REASONED; flux): Read receiver name and Ready condition without exposing webhookPath or condition messages; origin checks distinguish receiver rejection from proxy rejection. +- ingress-migration (REASONED; kubernetes): ingress-nginx retired in March 2026 with no subsequent updates; detect its labeled pods and migrate. Gateway API is recommended and Ingress API is frozen. +- gateway-install (REASONED; envoy, gateway, envoy-rolling): Install Envoy Gateway chart v1.9.1 and its default Gateway CRDs; separately create GatewayClass eg with the documented controllerName and confirm Accepted=True. +- gateway-alternatives (REASONED; traefik, cilium): Traefik uses providers.kubernetesGateway; Cilium uses gatewayAPI.enabled=true and requires kube-proxy replacement. +- gateway-tls (REASONED; gateway, envoy-rolling): Gateway HTTPS listener 443 terminates TLS using app-tls for app.example.com; HTTP 80 is for redirect and ACME challenges. +- gateway-route (REASONED; gateway): HTTPRoute app attaches only to the HTTPS listener via sectionName and forwards to the ClusterIP app Service on port 80. +- gateway-redirect (REASONED; gateway, envoy-rolling): The HTTP route uses RequestRedirect with scheme https and statusCode 301. +- cert-manager-enable (REASONED; cert-manager): From cert-manager 1.15, enable config.gatewayAPI.enabled; install Gateway CRDs before startup or restart cert-manager afterwards. +- cert-manager-issue (REASONED; cert-manager): Gateway issuer annotations produce Certificates per HTTPS-listener Secret using listener hostnames; ACME gatewayHTTPRoute parentRefs selects the challenge Gateway. +- basic-auth-policy (REASONED; gateway, envoy-rolling): Gateway API has no standard auth filter; Envoy SecurityPolicy attaches Basic auth to Gateway/HTTPRoute/GRPCRoute using an htpasswd Secret. +- basic-auth-hash (REASONED; apache, envoy-rolling): Envoy supports SHA hashes only; htpasswd -s is weak SHA-1, so use long random passwords over TLS and keep application login. +- basic-auth-input (REASONED; apache, envoy-rolling): htpasswd -i reads stdin, unlike argv-exposing -b; confirm twice, create with -cis, verify with -vi and only then create the Kubernetes Secret. +- oidc (REASONED; envoy-rolling): Envoy SecurityPolicy oidc uses issuer, clientID, clientSecret and redirectURL; enforce MFA at the identity provider. +- external-auth (REASONED; authelia, envoy-rolling): Authelia is an authorization endpoint, not a traffic proxy; Envoy extAuth.http targets its Service and /api/authz/ext-authz/. +- workload-exposure (REASONED; envoy-rolling, kubernetes): Expose workloads only through the Gateway LoadBalancer; keep databases on ClusterIP without routes and enforce pod access with NetworkPolicies plus database TLS/auth. +- secrets (REASONED; kubernetes): Store credentials in Secrets or an external operator, not committed ConfigMaps or environment literals; kubeconfigs containing credentials also need secret handling. +- eks-endpoint (REASONED; eks): EKS API defaults public; restrict public CIDRs or use private access. Private-only endpoints can resolve publicly to private VPC addresses. +- gke-endpoints (REASONED; gke): GKE authorized networks constrain IP-based access, not its separately IAM-gated DNS endpoint; inspect both and disable unused DNS access. +- aks-endpoint (REASONED; aks): Restrict AKS API access with authorized IP ranges; workload Gateway protection does not secure the control plane. +- kubeconfig (REASONED; kubernetes): Embedded tokens or client keys confer their RBAC access without another factor; exec-plugin configs may be only pointers, but args/env can themselves hold credentials. +- api-port (REASONED; kubernetes, eks): API server commonly uses 6443, but providers may use 443; use the complete kubeconfig endpoint and its actual port. +- etcd-ports (REASONED; kubernetes): etcd client/peer defaults are 2379/2380; require certificates, restrict holders and private access, and retain loopback listeners used by the local API server. +- etcd-storage (REASONED; kubernetes): API resources default to plaintext storage in etcd; valid client certificates are the boundary and etcd access is equivalent to cluster root. +- scheduler-port (REASONED; kubernetes): Scheduler default 10259 must remain restricted to cluster clients. +- controller-port (REASONED; kubernetes): Controller-manager default 10257 must remain restricted to cluster clients. +- proxy-port (REASONED; kubernetes): Worker kube-proxy health default 10256 needs separate node exposure checks. +- nodeports (REASONED; kubernetes): NodePort defaults span 30000 to 32767 TCP/UDP; Service listings do not actively probe that range. +- kubelet-legacy-anonymous (REASONED; kubelet): Legacy kubelet flags default anonymous-auth=true; explicitly disable it where flags control authentication. +- kubelet-legacy-webhook (REASONED; kubelet): Legacy authentication-token-webhook defaults false; explicitly enable it where flags configure authentication. +- kubelet-legacy-authz (REASONED; kubelet, kubernetes): Legacy authorization-mode defaults AlwaysAllow; set Webhook where flags configure authorization. +- kubelet-file-anonymous (REASONED; kubelet): v1beta1 KubeletConfiguration instead defaults authentication.anonymous.enabled=false. +- kubelet-file-webhook (REASONED; kubelet): v1beta1 KubeletConfiguration defaults authentication.webhook.enabled=true. +- kubelet-file-authz (REASONED; kubelet): v1beta1 KubeletConfiguration defaults authorization.mode=Webhook. +- kubelet-precedence (REASONED; kubelet): Explicit flags override --config; --config-dir drop-ins override defaults and the file. Inspect all effective settings together. +- kubeadm (REASONED; kubelet): kubeadm sets anonymous false, token webhook true, Webhook authorization and readOnlyPort 0; user overrides are preserved with warnings. +- eks-bootstrap (REASONED; eks-ami): The pinned EKS AMI bootstrap writes Anonymous.Enabled=false, Mode=Webhook and ReadOnlyPort=0; do not generalize to all managed nodes. +- kubelet-bind (REASONED; kubelet): Secured kubelet defaults to address 0.0.0.0 on 10250; wildcard help includes both families. Bind private addresses and restrict authorized callers. +- kubelet-exec (REASONED; kubelet): 10250 debugging handlers can execute in containers; authentication does not justify public reachability. +- readonly-default (REASONED; kubelet): Legacy read-only port defaults 10255, while v1beta1 readOnlyPort defaults 0; explicitly disable it and remove enabling overrides. +- readonly-auth (REASONED; kubelet): Read-only server uses plain HTTP with no authentication or authorization filter; its caller-supplied bind wiring was outside the inspected source subset. +- readonly-paths (REASONED; kubelet): Read-only handlers expose /pods, /stats/summary, metrics variants and health; v1.37.1 has no /spec handler. Securing 10250 does not secure 10255. +- gke-readonly (REASONED; gke-min): GKE disables the read-only port by default only for new clusters running 1.32+; inspect effective configuration on older or upgraded clusters. +- verify-services (REASONED; kubernetes, envoy-rolling, cert-manager): Only the Gateway should have NodePort/LoadBalancer exposure; verify its public address/DNS and Certificate Ready=True. +- verify-redirect (REASONED; gateway): HTTP should return 301 to the HTTPS application URL. +- verify-basic (REASONED; curl, envoy-rolling): Anonymous Basic-protected access should return 401; require SecurityPolicy Accepted=True and a valid credential reaching the app without the gateway Basic challenge. +- verify-api-target (REASONED; kubernetes): Use the whole kubeconfig endpoint, preserving IPv6 and inferring a schemeless endpoint from both cluster and user TLS settings, not a hardcoded 6443. +- verify-proxies (REASONED; curl): Disable curl config and configured proxies for direct probes; a transparent TLS middlebox can still answer for an unreachable target. +- verify-api-tcp (REASONED; kubernetes, eks): Outside allowed ranges, nc success proves something accepted TCP; corroborate failed probes with provider ranges and multiple vantage points. +- verify-api-http (REASONED; curl, eks): Any HTTP status with curl exit 0 means answered; exit 60 means a TLS peer answered, 7 failed connect, 28 is ambiguous timeout and 6 is DNS failure, not privacy proof. +- verify-node-scan (REASONED; kubernetes, nmap): Scan control-plane hosts first and every public node address in both families; workers alone miss API/etcd and separate etcd hosts are absent from kubectl nodes. +- verify-kubelet (REASONED; kubelet): Untrusted clients should reach neither kubelet port after isolation, while authorized clients still reach 10250; readOnlyPort=0 should leave no 10255 listener even for authorized clients. + +````json +{ + "schema": 1, + "checked": "2026-09-27", + "documentation_checked": "2026-09", + "body_sha256": "1e80b5ac6d9df2701bae1929db0d6cb2f9ce6416a58415b189d509640b8a44c8", + "components": { + "kubelet": { + "name": "Kubernetes kubelet/kubeadm source", + "basis": "v1.37.1", + "sources": { + "sc6056d6413a0": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L196-L224", + "s227d2a259fd3": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L373-L398", + "s568cd52cc674": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/apis/config/v1beta1/defaults.go#L85-L102", + "s2daf96dabbdf": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L281", + "s24ba323c3dab": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubelet/app/options/options.go#L282", + "sd81692a1876f": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L33-L48", + "s0eb30acf2f46": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L95-L100", + "s1c86aaef7307": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/kubelet.go#L144-L188", + "sb98bd327db92": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/cmd/kubeadm/app/componentconfigs/utils.go#L66-L71", + "s3fc405855b4a": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/staging/src/k8s.io/kubelet/config/v1beta1/types.go#L160-L176", + "s98cf98130201": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L181-L250", + "s8bea69523ed9": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L339-L375", + "sa02a895a475c": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/cluster/ports/ports.go#L32-L37", + "s6b753b247f8a": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L477-L572", + "sd00d4cdb9276": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/stats/handler.go#L109-L133", + "s02441dcae3cf": "https://github.com/kubernetes/kubernetes/blob/f78e722310e50bcaca9276be22276d9e91d91308/pkg/kubelet/server/server.go#L577-L610" + } + }, + "kubernetes": { + "name": "Kubernetes documentation", + "basis": "unknown", + "sources": { + "s0f11d6ed39b7": "https://kubernetes.io/blog/2026/01/29/ingress-nginx-statement/", + "sca23283c5a9b": "https://kubernetes.io/docs/concepts/services-networking/gateway/", + "sd8c8df3b9ddc": "https://kubernetes.io/docs/concepts/services-networking/ingress/", + "se41a53ea6778": "https://kubernetes.io/docs/reference/config-api/kubeconfig.v1/", + "sae633285120e": "https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/", + "sd59fb2f35cd7": "https://kubernetes.io/docs/reference/networking/ports-and-protocols/", + "s01f081fed0ff": "https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/", + "s49a29258961b": "https://kubernetes.io/docs/reference/access-authn-authz/kubelet-authn-authz/", + "s04f0c3099fd7": "https://kubernetes.io/docs/reference/kubectl/jsonpath/" + } + }, + "gateway": { + "name": "Gateway API documentation", + "basis": "unknown", + "sources": { + "se0e45c61dc65": "https://gateway-api.sigs.k8s.io/guides/getting-started/introduction/", + "se1f28d98d168": "https://gateway-api.sigs.k8s.io/guides/user-guides/tls/", + "s4a971425e1d1": "https://gateway-api.sigs.k8s.io/guides/user-guides/http-routing/", + "s8d02fbc742ba": "https://gateway-api.sigs.k8s.io/guides/user-guides/http-redirect-rewrite/" + } + }, + "envoy": { + "name": "Envoy Gateway", + "basis": "v1.9.1", + "sources": { + "s6412abbdfdbd": "https://github.com/envoyproxy/gateway/releases/download/v1.9.1/quickstart.yaml" + } + }, + "cert-manager": { + "name": "cert-manager Gateway support minimum", + "basis": "1.15", + "sources": { + "s72bdb97928a8": "https://cert-manager.io/docs/usage/gateway/", + "s8d82244c2d6d": "https://cert-manager.io/docs/configuration/acme/http01/" + } + }, + "apache": { + "name": "Apache htpasswd documentation", + "basis": "unknown", + "sources": { + "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" + } + }, + "authelia": { + "name": "Authelia documentation", + "basis": "unknown", + "sources": { + "s0cb4f49871b5": "https://www.authelia.com/integration/proxies/introduction/", + "scfa9e2cc2ef5": "https://www.authelia.com/integration/kubernetes/envoy/gateway/" + } + }, + "eks": { + "name": "Amazon EKS documentation", + "basis": "unknown", + "sources": { + "s9724ce882930": "https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html" + } + }, + "eks-ami": { + "name": "Amazon EKS AMI bootstrap", + "basis": "6caf8311a3c6a8da71ac7e5e83f9c2e06287039a", + "sources": { + "sbe1fce93c7b2": "https://github.com/awslabs/amazon-eks-ami/blob/6caf8311a3c6a8da71ac7e5e83f9c2e06287039a/nodeadm/internal/kubelet/config.go" + } + }, + "gke": { + "name": "GKE documentation", + "basis": "unknown", + "sources": { + "s0f44d89575ed": "https://docs.cloud.google.com/kubernetes-engine/docs/concepts/network-isolation#how_authorized_networks_work" + } + }, + "gke-min": { + "name": "GKE new-cluster read-only-port boundary", + "basis": "1.32", + "sources": { + "s4c7be4adcab4": "https://docs.cloud.google.com/kubernetes-engine/docs/how-to/disable-kubelet-readonly-port" + } + }, + "aks": { + "name": "AKS documentation", + "basis": "unknown", + "sources": { + "s897dd27ab9a1": "https://learn.microsoft.com/en-us/azure/aks/api-server-authorized-ip-ranges" + } + }, + "traefik": { + "name": "Traefik documentation", + "basis": "unknown", + "sources": { + "s06f2ee45331e": "https://doc.traefik.io/traefik/reference/install-configuration/providers/kubernetes/kubernetes-gateway/" + } + }, + "cilium": { + "name": "Cilium documentation", + "basis": "unknown", + "sources": { + "sacaa7a78461d": "https://docs.cilium.io/en/stable/network/servicemesh/gateway-api/gateway-api/" + } + }, + "envoy-rolling": { + "name": "Envoy Gateway documentation (rolling)", + "basis": "unknown", + "sources": { + "se833317feea7": "https://gateway.envoyproxy.io/docs/tasks/security/secure-gateways/", + "sb5fa4f4ffa1a": "https://gateway.envoyproxy.io/docs/tasks/security/basic-auth/", + "s7fdd76c5d549": "https://gateway.envoyproxy.io/docs/tasks/security/oidc/", + "s100049c74adf": "https://gateway.envoyproxy.io/docs/tasks/security/ext-auth/", + "s9b1d646cb718": "https://gateway.envoyproxy.io/docs/tasks/traffic/http-redirect/", + "s881f08174c54": "https://gateway.envoyproxy.io/v1.9/install/install-helm/", + "s9ad0f418b20b": "https://gateway.envoyproxy.io/v1.9/tasks/quickstart/" + } + }, + "curl": { + "name": "curl documentation", + "basis": "unknown", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html", + "s7d4a6e627b1b": "https://curl.se/libcurl/c/libcurl-errors.html" + } + }, + "nmap": { + "name": "Nmap documentation", + "basis": "unknown", + "sources": { + "sb7d0e7eb5141": "https://nmap.org/book/man-host-discovery.html", + "s1818abeb3f92": "https://nmap.org/book/man-port-specification.html", + "s79c95712dd8d": "https://nmap.org/book/man-misc-options.html" + } + } + }, + "claims": { + "ingress-migration": {"text": "ingress-nginx retired in March 2026 with no subsequent updates; detect its labeled pods and migrate. Gateway API is recommended and Ingress API is frozen.", "components": ["kubernetes"], "sources": ["kubernetes:s0f11d6ed39b7", "kubernetes:sca23283c5a9b", "kubernetes:sd8c8df3b9ddc"], "status": "REASONED"}, + "gateway-install": {"text": "Install Envoy Gateway chart v1.9.1 and its default Gateway CRDs; separately create GatewayClass eg with the documented controllerName and confirm Accepted=True.", "components": ["envoy", "gateway", "envoy-rolling"], "sources": ["envoy-rolling:s881f08174c54", "envoy:s6412abbdfdbd", "gateway:se0e45c61dc65"], "status": "REASONED"}, + "gateway-alternatives": {"text": "Traefik uses providers.kubernetesGateway; Cilium uses gatewayAPI.enabled=true and requires kube-proxy replacement.", "components": ["traefik", "cilium"], "sources": ["traefik:s06f2ee45331e", "cilium:sacaa7a78461d"], "status": "REASONED"}, + "gateway-tls": {"text": "Gateway HTTPS listener 443 terminates TLS using app-tls for app.example.com; HTTP 80 is for redirect and ACME challenges.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:se1f28d98d168", "envoy-rolling:se833317feea7"], "status": "REASONED"}, + "gateway-route": {"text": "HTTPRoute app attaches only to the HTTPS listener via sectionName and forwards to the ClusterIP app Service on port 80.", "components": ["gateway"], "sources": ["gateway:s4a971425e1d1"], "status": "REASONED"}, + "gateway-redirect": {"text": "The HTTP route uses RequestRedirect with scheme https and statusCode 301.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:s8d02fbc742ba", "envoy-rolling:s9b1d646cb718"], "status": "REASONED"}, + "cert-manager-enable": {"text": "From cert-manager 1.15, enable config.gatewayAPI.enabled; install Gateway CRDs before startup or restart cert-manager afterwards.", "components": ["cert-manager"], "sources": ["cert-manager:s72bdb97928a8"], "status": "REASONED"}, + "cert-manager-issue": {"text": "Gateway issuer annotations produce Certificates per HTTPS-listener Secret using listener hostnames; ACME gatewayHTTPRoute parentRefs selects the challenge Gateway.", "components": ["cert-manager"], "sources": ["cert-manager:s72bdb97928a8", "cert-manager:s8d82244c2d6d"], "status": "REASONED"}, + "basic-auth-policy": {"text": "Gateway API has no standard auth filter; Envoy SecurityPolicy attaches Basic auth to Gateway/HTTPRoute/GRPCRoute using an htpasswd Secret.", "components": ["gateway", "envoy-rolling"], "sources": ["gateway:s4a971425e1d1", "envoy-rolling:sb5fa4f4ffa1a"], "status": "REASONED"}, + "basic-auth-hash": {"text": "Envoy supports SHA hashes only; htpasswd -s is weak SHA-1, so use long random passwords over TLS and keep application login.", "components": ["apache", "envoy-rolling"], "sources": ["envoy-rolling:sb5fa4f4ffa1a", "apache:scde9004bb967"], "status": "REASONED"}, + "basic-auth-input": {"text": "htpasswd -i reads stdin, unlike argv-exposing -b; confirm twice, create with -cis, verify with -vi and only then create the Kubernetes Secret.", "components": ["apache", "envoy-rolling"], "sources": ["apache:scde9004bb967", "envoy-rolling:sb5fa4f4ffa1a"], "status": "REASONED"}, + "oidc": {"text": "Envoy SecurityPolicy oidc uses issuer, clientID, clientSecret and redirectURL; enforce MFA at the identity provider.", "components": ["envoy-rolling"], "sources": ["envoy-rolling:s7fdd76c5d549"], "status": "REASONED"}, + "external-auth": {"text": "Authelia is an authorization endpoint, not a traffic proxy; Envoy extAuth.http targets its Service and /api/authz/ext-authz/.", "components": ["authelia", "envoy-rolling"], "sources": ["authelia:s0cb4f49871b5", "authelia:scfa9e2cc2ef5", "envoy-rolling:s100049c74adf"], "status": "REASONED"}, + "workload-exposure": {"text": "Expose workloads only through the Gateway LoadBalancer; keep databases on ClusterIP without routes and enforce pod access with NetworkPolicies plus database TLS/auth.", "components": ["envoy-rolling", "kubernetes"], "sources": ["envoy-rolling:s9ad0f418b20b", "kubernetes:sca23283c5a9b"], "status": "REASONED"}, + "secrets": {"text": "Store credentials in Secrets or an external operator, not committed ConfigMaps or environment literals; kubeconfigs containing credentials also need secret handling.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778", "kubernetes:sae633285120e"], "status": "REASONED"}, + "eks-endpoint": {"text": "EKS API defaults public; restrict public CIDRs or use private access. Private-only endpoints can resolve publicly to private VPC addresses.", "components": ["eks"], "sources": ["eks:s9724ce882930"], "status": "REASONED"}, + "gke-endpoints": {"text": "GKE authorized networks constrain IP-based access, not its separately IAM-gated DNS endpoint; inspect both and disable unused DNS access.", "components": ["gke"], "sources": ["gke:s0f44d89575ed"], "status": "REASONED"}, + "aks-endpoint": {"text": "Restrict AKS API access with authorized IP ranges; workload Gateway protection does not secure the control plane.", "components": ["aks"], "sources": ["aks:s897dd27ab9a1"], "status": "REASONED"}, + "kubeconfig": {"text": "Embedded tokens or client keys confer their RBAC access without another factor; exec-plugin configs may be only pointers, but args/env can themselves hold credentials.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778"], "status": "REASONED"}, + "api-port": {"text": "API server commonly uses 6443, but providers may use 443; use the complete kubeconfig endpoint and its actual port.", "components": ["kubernetes", "eks"], "sources": ["kubernetes:sd59fb2f35cd7", "eks:s9724ce882930"], "status": "REASONED"}, + "etcd-ports": {"text": "etcd client/peer defaults are 2379/2380; require certificates, restrict holders and private access, and retain loopback listeners used by the local API server.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7", "kubernetes:s01f081fed0ff"], "status": "REASONED"}, + "etcd-storage": {"text": "API resources default to plaintext storage in etcd; valid client certificates are the boundary and etcd access is equivalent to cluster root.", "components": ["kubernetes"], "sources": ["kubernetes:sae633285120e", "kubernetes:s01f081fed0ff"], "status": "REASONED"}, + "scheduler-port": {"text": "Scheduler default 10259 must remain restricted to cluster clients.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, + "controller-port": {"text": "Controller-manager default 10257 must remain restricted to cluster clients.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, + "proxy-port": {"text": "Worker kube-proxy health default 10256 needs separate node exposure checks.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, + "nodeports": {"text": "NodePort defaults span 30000 to 32767 TCP/UDP; Service listings do not actively probe that range.", "components": ["kubernetes"], "sources": ["kubernetes:sd59fb2f35cd7"], "status": "REASONED"}, + "kubelet-legacy-anonymous": {"text": "Legacy kubelet flags default anonymous-auth=true; explicitly disable it where flags control authentication.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0", "kubelet:s227d2a259fd3"], "status": "REASONED"}, + "kubelet-legacy-webhook": {"text": "Legacy authentication-token-webhook defaults false; explicitly enable it where flags configure authentication.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0"], "status": "REASONED"}, + "kubelet-legacy-authz": {"text": "Legacy authorization-mode defaults AlwaysAllow; set Webhook where flags configure authorization.", "components": ["kubelet", "kubernetes"], "sources": ["kubelet:sc6056d6413a0", "kubernetes:s49a29258961b"], "status": "REASONED"}, + "kubelet-file-anonymous": {"text": "v1beta1 KubeletConfiguration instead defaults authentication.anonymous.enabled=false.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, + "kubelet-file-webhook": {"text": "v1beta1 KubeletConfiguration defaults authentication.webhook.enabled=true.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, + "kubelet-file-authz": {"text": "v1beta1 KubeletConfiguration defaults authorization.mode=Webhook.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674"], "status": "REASONED"}, + "kubelet-precedence": {"text": "Explicit flags override --config; --config-dir drop-ins override defaults and the file. Inspect all effective settings together.", "components": ["kubelet"], "sources": ["kubelet:s2daf96dabbdf", "kubelet:s24ba323c3dab"], "status": "REASONED"}, + "kubeadm": {"text": "kubeadm sets anonymous false, token webhook true, Webhook authorization and readOnlyPort 0; user overrides are preserved with warnings.", "components": ["kubelet"], "sources": ["kubelet:sd81692a1876f", "kubelet:s0eb30acf2f46", "kubelet:s1c86aaef7307", "kubelet:sb98bd327db92"], "status": "REASONED"}, + "eks-bootstrap": {"text": "The pinned EKS AMI bootstrap writes Anonymous.Enabled=false, Mode=Webhook and ReadOnlyPort=0; do not generalize to all managed nodes.", "components": ["eks-ami"], "sources": ["eks-ami:sbe1fce93c7b2"], "status": "REASONED"}, + "kubelet-bind": {"text": "Secured kubelet defaults to address 0.0.0.0 on 10250; wildcard help includes both families. Bind private addresses and restrict authorized callers.", "components": ["kubelet"], "sources": ["kubelet:s568cd52cc674", "kubelet:s3fc405855b4a", "kubelet:s227d2a259fd3", "kubelet:s98cf98130201"], "status": "REASONED"}, + "kubelet-exec": {"text": "10250 debugging handlers can execute in containers; authentication does not justify public reachability.", "components": ["kubelet"], "sources": ["kubelet:s8bea69523ed9", "kubelet:s3fc405855b4a", "kubelet:s98cf98130201", "kubelet:s02441dcae3cf"], "status": "REASONED"}, + "readonly-default": {"text": "Legacy read-only port defaults 10255, while v1beta1 readOnlyPort defaults 0; explicitly disable it and remove enabling overrides.", "components": ["kubelet"], "sources": ["kubelet:sc6056d6413a0", "kubelet:sa02a895a475c", "kubelet:s3fc405855b4a"], "status": "REASONED"}, + "readonly-auth": {"text": "Read-only server uses plain HTTP with no authentication or authorization filter; its caller-supplied bind wiring was outside the inspected source subset.", "components": ["kubelet"], "sources": ["kubelet:s98cf98130201", "kubelet:s8bea69523ed9"], "status": "REASONED"}, + "readonly-paths": {"text": "Read-only handlers expose /pods, /stats/summary, metrics variants and health; v1.37.1 has no /spec handler. Securing 10250 does not secure 10255.", "components": ["kubelet"], "sources": ["kubelet:s6b753b247f8a", "kubelet:sd00d4cdb9276"], "status": "REASONED"}, + "gke-readonly": {"text": "GKE disables the read-only port by default only for new clusters running 1.32+; inspect effective configuration on older or upgraded clusters.", "components": ["gke-min"], "sources": ["gke-min:s4c7be4adcab4"], "status": "REASONED"}, + "verify-services": {"text": "Only the Gateway should have NodePort/LoadBalancer exposure; verify its public address/DNS and Certificate Ready=True.", "components": ["kubernetes", "envoy-rolling", "cert-manager"], "sources": ["kubernetes:s04f0c3099fd7", "envoy-rolling:s9ad0f418b20b", "cert-manager:s72bdb97928a8"], "status": "REASONED", "verify": [1]}, + "verify-redirect": {"text": "HTTP should return 301 to the HTTPS application URL.", "components": ["gateway"], "sources": ["gateway:s8d02fbc742ba"], "status": "REASONED", "verify": [1]}, + "verify-basic": {"text": "Anonymous Basic-protected access should return 401; require SecurityPolicy Accepted=True and a valid credential reaching the app without the gateway Basic challenge.", "components": ["curl", "envoy-rolling"], "sources": ["envoy-rolling:sb5fa4f4ffa1a", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, + "verify-api-target": {"text": "Use the whole kubeconfig endpoint, preserving IPv6 and inferring a schemeless endpoint from both cluster and user TLS settings, not a hardcoded 6443.", "components": ["kubernetes"], "sources": ["kubernetes:se41a53ea6778", "kubernetes:sd59fb2f35cd7"], "status": "REASONED", "verify": [1]}, + "verify-proxies": {"text": "Disable curl config and configured proxies for direct probes; a transparent TLS middlebox can still answer for an unreachable target.", "components": ["curl"], "sources": ["curl:s2b2686afaf41", "curl:s7d4a6e627b1b"], "status": "REASONED", "verify": [1]}, + "verify-api-tcp": {"text": "Outside allowed ranges, nc success proves something accepted TCP; corroborate failed probes with provider ranges and multiple vantage points.", "components": ["kubernetes", "eks"], "sources": ["kubernetes:sd59fb2f35cd7", "eks:s9724ce882930"], "status": "REASONED", "verify": [1]}, + "verify-api-http": {"text": "Any HTTP status with curl exit 0 means answered; exit 60 means a TLS peer answered, 7 failed connect, 28 is ambiguous timeout and 6 is DNS failure, not privacy proof.", "components": ["curl", "eks"], "sources": ["curl:s7d4a6e627b1b", "eks:s9724ce882930"], "status": "REASONED", "verify": [1]}, + "verify-node-scan": {"text": "Scan control-plane hosts first and every public node address in both families; workers alone miss API/etcd and separate etcd hosts are absent from kubectl nodes.", "components": ["kubernetes", "nmap"], "sources": ["kubernetes:sd59fb2f35cd7", "kubernetes:s01f081fed0ff", "nmap:sb7d0e7eb5141", "nmap:s1818abeb3f92", "nmap:s79c95712dd8d"], "status": "REASONED", "verify": [1]}, + "verify-kubelet": {"text": "Untrusted clients should reach neither kubelet port after isolation, while authorized clients still reach 10250; readOnlyPort=0 should leave no 10255 listener even for authorized clients.", "components": ["kubelet"], "sources": ["kubelet:s98cf98130201", "kubelet:s3fc405855b4a"], "status": "REASONED", "verify": [1]} + } +} +```` +====================================================================== +==> gitops-controllers.md +====================================================================== + +# GitOps controllers: Argo CD and Flux + +Written against Argo CD v3.5.3 source (pinned to a commit), Flux OIDC introduction v2.9, Dex v2.45.1 source v2.45.1, Redis 8.2.3 source 8.2.3, and curl curl-8_14_1; 1 further source is pinned to a commit and 3 carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 50 claims, all reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + Argo CD and Flux reconcile a git repository into a Kubernetes cluster, so both hold cluster-admin-equivalent power: whoever can drive the controller, or change what it reconciles, can deploy anything it can reach, across the clusters it is registered against and to the extent the controller's Kubernetes RBAC, its registered-cluster credentials, and project restrictions allow. Three exposures follow @@ -8330,69 +10105,308 @@ they were not rechecked online. Remaining source limits are identified above. - Argo CD additive default-role enforcement (v3.5.3; commit c9c369efcc5b2a0bd720803f8d14a1c3eaddf579; checked October 2026): https://github.com/argoproj/argo-cd/blob/v3.5.3/util/rbac/rbac.go#L401-L406 - Argo CD default-role ConfigMap loading (v3.5.3; commit c9c369efcc5b2a0bd720803f8d14a1c3eaddf579; checked October 2026): https://github.com/argoproj/argo-cd/blob/v3.5.3/util/rbac/rbac.go#L540-L541 -====================================================================== -==> self-hosted-ci-runners.md -====================================================================== - -# Self-hosted CI runners: GitHub Actions and GitLab Runner +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/gitops-controllers.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + **Version basis** -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. -Metadata reviewed 2026-10-12; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- github: GitHub Actions documentation (unknown) -- github-api: GitHub REST API (2026-03-10) -- gitlab: GitLab rolling documentation (unknown) -- python: Python tomllib minimum (3.11) -- docker: Docker Engine documentation (unknown) -- aws: AWS EC2 documentation (unknown) +- argo: Argo CD v3.5.3 source (c9c369efcc5b2a0bd720803f8d14a1c3eaddf579) +- flux-oidc: Flux OIDC introduction (v2.9) +- argo-docs: Argo CD documentation (unknown) +- flux: Flux documentation (unknown) +- dex: Dex v2.45.1 source (v2.45.1) +- redis-image: docker-library-redis v8.2.3 source (bcbc692165ce2241e194035041c5b5e01d2e1a5f) +- redis-source: Redis 8.2.3 source (8.2.3) +- curl: curl (curl-8_14_1) +- python: Python documentation (unknown) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- job-transport (REASONED; github, gitlab): Both runners poll over HTTPS without an inbound job-polling port; verify GitLab TLS and restrict separate autoscaler management or webhook listeners. -- worker-account (REASONED; github, gitlab, docker): Use a dedicated least-privileged account, keep provisioning credentials off workers and confine rootful Docker access to disposable VMs. -- worker-egress (REASONED; github, gitlab, aws): Enforce job egress outside job control, isolate production and other workers, and deny unused metadata over both families; IMDSv2 does not stop arbitrary authorized job code. -- github-eligibility (REASONED; github): Keep self-hosted runners off public repositories; runner groups admit only private repositories by default and the public override should stay off. -- fork-token (REASONED; github): Ordinary fork pull_request jobs normally receive a read-only GITHUB_TOKEN and no other secrets, but can still read host credentials and reachable services. -- privileged-triggers (REASONED; github): pull_request_target and workflow_run can access secrets and write tokens; never execute untrusted PR code, artifacts or caches in these jobs. -- private-forks (REASONED; github): Review private-repository fork settings, which can grant write tokens and secrets; separate trusted privileged workers from untrusted CI. -- registration-token (REASONED; github-api): Registration tokens last one hour; generate on demand, never bake into images, and do not confuse expiry with runner deregistration. -- registration-manager (REASONED; github-api): Keep the App/PAT generating registration or JIT credentials off workers; organization endpoints need Self-hosted runners: write, repository endpoints Administration: write. -- jit (REASONED; github-api): Protect encoded_jit_config and the whole response; JIT runners execute at most one job. -- runner-credential (REASONED; github): Protect the installed runner authentication credential, directory and backups. -- github-job-token (REASONED; github): GITHUB_TOKEN is repository-scoped and per-job; set read-only defaults and grant only trusted jobs additional permissions. -- oidc (REASONED; github): Grant id-token: write only where needed; the cloud trust must restrict issuer, audience and repository/branch or protected-environment subject. -- github-lifecycle (REASONED; github, github-api): Prefer ephemeral/JIT workers and destroy their machines and storage; deregistration does not destroy disks, and labels route rather than authorize jobs. -- shell-executor (REASONED; gitlab): The GitLab shell executor runs as the runner user without isolation; avoid it for untrusted jobs on shared hosts. -- docker-executor (REASONED; gitlab, docker): Use unprivileged Docker; privileged=true or services_privileged and host Docker socket/TCP access can expose the host despite privileged=false. -- executor-host-access (REASONED; gitlab): Deny sensitive host mounts, host PID/network namespaces, unnecessary devices and capabilities; build without the host daemon. -- kubernetes-executor (REASONED; gitlab): Enforce nonprivileged containers, no privilege escalation, scoped ServiceAccounts and no unnecessary token mounting; prevent job overrides. -- gitlab-disposal (REASONED; gitlab): Containers share the kernel; use disposable separate VMs/nodes as needed and one-job autoscaling workers. Instance and Docker Autoscaler GA is recorded as Runner 17.1. -- gitlab-registration (REASONED; gitlab): Legacy registration tokens were disabled by default in GitLab 17.0, with removal scheduled for 20.0; use runner authentication tokens. -- gitlab-runner-token (REASONED; gitlab): Protect and rotate the runner authentication token in config.toml and backups; a cloned runner can steal assigned jobs. -- gitlab-creation (REASONED; gitlab): POST /user/runners needs create_runner scope; keep that access token off workers and set scope/protection at creation. -- gitlab-job-token (REASONED; gitlab): CI_JOB_TOKEN carries supported triggering-user access during the job and appears as CI_REGISTRY_PASSWORD and inside CI_REPOSITORY_URL. -- gitlab-allowlist (REASONED; gitlab): Target-project inbound job-token allowlists limit who may access that target, not job egress; also restrict resource feature visibility. -- gitlab-forks (REASONED; gitlab): Instance runners on public installations accept strangers; project scope does not establish code trust. Review parent-launched fork pipelines or disable ci_allow_fork_pipelines_to_run_in_parent_project. -- gitlab-protected (REASONED; gitlab): Developer access can compromise persistent runners; release runners need ref_protected and a pool separate from fork-merge-request testing. -- gitlab-metrics (REASONED; gitlab): Optional listen_address metrics and profiling have no built-in authorization; 9252 is allocated, not proof of the configured port. -- gitlab-session (REASONED; gitlab): Optional session_server uses TLS and must admit required GitLab traffic at its configured address; 8093 is an example, not an established default. -- verify-eligibility (REASONED; github-api): Fully paginate groups, selected repositories and public repositories' own runners; visibility=all alone is not public eligibility, and inherited access needs review. -- verify-permissions (REASONED; github): Inspect organization and repository workflow-token defaults and PR approval settings; explicit workflow permissions can raise grants and id-token is separate. -- verify-assignment (REASONED; github-api, github): A controlled fork workflow should run when exposed and lose assignment when restricted, while an allowed private job still runs; skipped/pending/offline/timeout is inconclusive. -- verify-config (DEMONSTRATED; python, gitlab): Python 3.14.4 synthetic fixtures reported exposed/fixed Docker, Kubernetes and autoscaler values; wrong types passed through, misplaced keys printed null, and empty/error cases differed in output and exit status. -- verify-daemon (REASONED; gitlab, docker): Resolve docker.host through DOCKER_HOST or the default socket when absent; flag actual job access to a rootful daemon, not tcp:// alone. -- verify-autoscaler (REASONED; gitlab): For Instance/Docker Autoscaler require capacity_per_instance=1 and max_use_count=1 explicitly as integers; absent or other values are findings. -- verify-gitlab-api (REASONED; gitlab): Read inbound allowlists and require release-runner ref_protected against exposed controls; gitlab-runner verify proves authentication only. -- verify-lifecycle (REASONED; github-api, gitlab, github): Check complete deregistration listings and infrastructure disposal; persistent-worker canary must survive in the control, but absence alone does not prove destruction or clean shared caches. -- verify-listeners (REASONED; gitlab): Inventory namespaces and publications, require the permitted-client response, then test disallowed access; any HTTP response proves reachability, failure needs corroborating firewall denial. +- git-boundary (REASONED; argo-docs, flux): Repository writers exercise reconciliation permissions; review platform changes, separate tenants and scope credentials. +- secret-storage (REASONED; argo-docs, flux): Protect bootstrap, registry, cluster, webhook and decryption credentials; commit only encrypted secrets and keep SOPS/age keys out of Git. +- egress (REASONED; argo-docs, flux): Restrict fetch, API, identity and notification egress; Argo repository allowlists do not constrain remote bases or Helm dependencies. +- argo-bootstrap (REASONED; argo-docs): Rotate the initial admin password before deleting argocd-initial-admin-secret; deletion alone does not rotate it. +- argo-sso (REASONED; argo-docs): The built-in admin is unrestricted; configure named SSO roles and IdP MFA, then set admin.enabled=false. +- argo-origin (REASONED; argo-docs, argo): Keep the ClusterIP origin private behind authenticated TLS and protect both REST/browser and CLI/gRPC routes. +- argo-server (REASONED; argo): Server defaults to 0.0.0.0:8080; the Service maps 80 and 443 to it. +- argo-tls (REASONED; argo, argo-docs): Server --insecure/server.insecure defaults false and disables backend TLS, not authentication; use only behind protected TLS termination. +- argo-client-tls (REASONED; argo-docs): argocd login --insecure skips client certificate verification; the server initially uses a self-signed certificate. +- argo-metrics (REASONED; argo): Server HTTP metrics on 8083 are unauthenticated and bind to --address, not --metrics-address. +- argo-profiler (REASONED; argo): Server and repo-server pprof paths return 401 unless the profiler file contains exactly true with no newline; enabled profiling has no caller authentication. +- argo-profiler-file (REASONED; argo): ARGOCD_ENABLE_PROFILER_FILE_PATH defaults to /home/argocd/params/profiler.enabled; the server mounts ConfigMap key server.profile.enabled there. +- repo-grpc (REASONED; argo): Repo-server gRPC binds 0.0.0.0:8081 with TLS by default; mTLS is skipped without the client CA, separate from the serving certificate. +- repo-http (REASONED; argo): Repo-server HTTP metrics and health bind 0.0.0.0:8084 without an authentication wrapper. +- repo-limits (REASONED; argo): Repo-server requires verified client certificates with a loaded client CA; missing or unstatable serving files trigger a session self-signed certificate, while an invalid present pair errors. No authorization interceptor appears; service method authorization is unverified. +- controller-metrics (REASONED; argo): Application-controller metrics and health use 8082; the command passes that port to the controller, but the fetched files do not establish the bind address. +- applicationset-webhook (REASONED; argo): ApplicationSet webhook binds :7000; its container and Service declare 7000. +- applicationset-metrics (REASONED; argo): ApplicationSet metrics bind :8080; its container and Service declare 8080. +- applicationset-health (REASONED; argo): ApplicationSet health probes bind :8081 without a container or Service port declaration; missing declarations do not close sockets. +- notifications (REASONED; argo): Notifications HTTP metrics bind 0.0.0.0:9001 without authentication; a metrics Service and TCP liveness probe exist without containerPort. +- dex-http (REASONED; argo): Dex starts only with nonempty generated configuration; HTTP defaults to TLS on 0.0.0.0:5556 using /tmp/tls.crt and /tmp/tls.key. +- dex-grpc (REASONED; argo, dex): Generated Dex gRPC uses 0.0.0.0:5557 without TLS or client-auth settings; Dex v2.45.1 source sets no transport credentials or authentication interceptor for this configuration, while API method authorization is unverified; this assumes the deployed ghcr.io/dexidp/dex:v2.45.1 tag was built from Dex v2.45.1. +- dex-telemetry (REASONED; argo): Dex telemetry uses HTTP on 0.0.0.0:5558; all three Dex ports have Services. +- redis-password (REASONED; argo, redis-image, redis-source): Redis receives --requirepass from argocd-redis/auth; secret-init generates and checks that Secret. The redis:8.2.3-alpine tag, taken as built from docker-library-redis v8.2.3, uses Redis wildcard bind defaults and patches protected mode off. +- redis-tls (REASONED; argo): Base manifests configure no Redis TLS and the Argo Redis client defaults --redis-use-tls=false; protect traffic, Secret and cache. +- component-policies (REASONED; argo): Base policies restrict repo gRPC, Redis and Dex callers by component; enforcement needs a supporting CNI. +- broad-policies (REASONED; argo): Server policy allows all ingress; other metrics and ApplicationSet webhook allowances admit every namespace. Narrow existing additive allowances and egress separately. +- argo-anonymous (REASONED; argo, argo-docs): users.anonymous.enabled defaults off; anonymous callers otherwise inherit policy.default. +- argo-disable-auth (REASONED; argo): server.disable.auth defaults false; inspect effective args and environment because anonymous-off does not compensate for disabled authentication. +- argo-default-role (REASONED; argo, argo-docs): Base RBAC ConfigMap omits policy.default; the inspected loader supplies an empty role to the enforcer, which skips its additive default-role check. Default-role grants are additive and cannot be revoked by a later subject rule; define scoped policy.csv roles and group mappings. Server-side wiring remains unverified. +- argo-webhook (REASONED; argo-docs): /api/webhook accepts unauthenticated refresh events without a shared secret; configure one to limit spoofed reconciliation and resource exhaustion. +- argo-projects (REASONED; argo-docs): Restrict AppProjects including the built-in default project; project constraints do not reduce a compromised controller's Kubernetes credentials. +- argo-cluster-rbac (REASONED; argo-docs): Reduce controller and registered-cluster argocd-manager write permissions to needed namespaces/resources while retaining required read access. +- flux-api (REASONED; flux): Flux has no standalone management login/API; Kubernetes authenticates its custom-resource operations, while separate dashboards need their own controls. +- flux-bootstrap (REASONED; flux): GitHub bootstrap --token-auth retains a PAT in flux-system; --token-auth=false creates a read-only SSH deploy key by default. Protect bootstrap input and Secrets. +- flux-writeback (REASONED; flux): Image automation needs Git write access, including --read-write-key=true for the bootstrap key; scope the repository and protect update branches and objects. +- flux-artifacts (REASONED; flux): Source-controller serves fetched artifacts over internal HTTP; enforced NetworkPolicy, not ClusterIP alone, isolates them. +- flux-receiver-port (REASONED; flux): Webhook receiver listens on 9292 behind Service port 80; external exposure needs a route and TLS termination. +- flux-receiver-types (REASONED; flux): generic validates nothing; generic-hmac, GitHub, Bitbucket and Nexus use HMAC, while gitlab compares X-Gitlab-Token. +- flux-oidc (REASONED; flux, flux-oidc): generic-oidc, introduced in Flux 2.9, rejects secretRef and validates bearer tokens; constrain issuer validations and audience, whose default is notification-controller. +- flux-rbac (REASONED; flux): Only kustomize-controller and helm-controller hold cluster-admin; other controllers still have meaningful permissions including Secret access. +- flux-lockdown (REASONED; flux): Use scoped reconciliation ServiceAccounts, --no-cross-namespace-refs and --no-remote-bases; --default-service-account fills an omission, not an explicit serviceAccountName. +- verify-argo (REASONED; argo-docs, argo, curl): Anonymous application listing should fail with 401 while a valid token lists the expected application; repeat at origin and CLI/gRPC and inspect effective RBAC. +- verify-bootstrap (REASONED; argo-docs): Absence of the bootstrap Secret proves removal of that material, not password rotation. +- verify-inventory (REASONED; argo, flux): Inventory Services, routes, host ports, policies and component sockets; node-local ss and policy objects alone do not establish cluster isolation. +- verify-repo-metrics (REASONED; argo): Base policy permits HTTP 200 metrics to monitoring and unrelated pods; narrowed policy should admit only monitoring, with a successful positive control. +- verify-server-metrics (REASONED; argo): Repeat on server 8083: only authorized monitoring should receive metrics after narrowing ingress; a failed control is inconclusive. +- verify-profiler (REASONED; argo): From an allowed pod, pprof cmdline should return 401 disabled, 200 for exact true, then 401 for true plus newline; keep metrics as control. +- verify-flux (REASONED; flux, python, curl): Identical generic-hmac bodies without and with X-Signature should be rejected and accepted with reconciliation; status is type/version dependent. The cited curl curl-8_14_1 lines show --data-binary posting data exactly as specified, preserving newlines and carriage returns that --data strips from @file input, reading @- from stdin and @filename from a file, and posting like --data's POST request. +- verify-receiver-status (REASONED; flux): Read receiver name and Ready condition without exposing webhookPath or condition messages; origin checks distinguish receiver rejection from proxy rejection. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "908b108054ff323b7d30f6ec6652728a0d065571804b124943b579b8f09f77cc", + "components": { + "argo": { + "name": "Argo CD v3.5.3 source", + "basis": "c9c369efcc5b2a0bd720803f8d14a1c3eaddf579", + "sources": { + "sddf4853ad503": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-service.yaml#L9-L20", + "s6e8c96d23e05": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-server/commands/argocd_server.go#L307-L323", + "s96065cc09776": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/server.go#L512-L535", + "s7cc690d1bf69": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/server.go#L668-L677", + "s028a4769d6c3": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/server/metrics/metrics.go#L74-L100", + "sc52bbd14bff4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/profile/profile.go#L11-L30", + "s98b27313f615": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-repo-server/commands/argocd_repo_server.go#L180-L212", + "sb0648e746fb9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L406-L408", + "s2f5af86c62d1": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L475-L481", + "s9bca471ac6d9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-repo-server/commands/argocd_repo_server.go#L257-L280", + "s4bbc848d9923": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/repo-server/argocd-repo-server-deployment.yaml#L367-L384", + "sf98b6bc4f5dc": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/application-controller/argocd-application-controller-statefulset.yaml#L360-L365", + "sf4dd846c82c4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/common/common.go#L75-L92", + "s49a7c83d9371": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-applicationset-controller/commands/applicationset_controller.go#L294-L296", + "s97a588d8a83f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/applicationset-controller/argocd-applicationset-controller-service.yaml#L9-L20", + "s0a85b38dfca8": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-notification/commands/argocd_notification.go#L149-L154", + "s5e141e32ed9c": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/notification/argocd-notifications-controller-metrics-service.yaml#L9-L16", + "s8efcc66f9cb4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-dex/commands/argocd_dex.go#L82-L119", + "s30c391d9dc23": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-dex/commands/argocd_dex.go#L143-L146", + "se477de2fdfa0": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/dex/config.go#L16-L152", + "s56c41c02ba8b": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-service.yaml#L9-L25", + "s996c72c5db28": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/redis/argocd-redis-deployment.yaml#L18-L58", + "s34909d7a1932": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/cache/cache.go#L233-L238", + "s1a13cc06db6d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/repo-server/argocd-repo-server-network-policy.yaml#L9-L35", + "s4bbf38e83770": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/redis/argocd-redis-network-policy.yaml#L9-L28", + "sadccbbcffcd8": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-network-policy.yaml#L9-L29", + "s743c8187cbd7": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-network-policy.yaml#L9-L16", + "s49f63c21c3cd": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/application-controller/argocd-application-controller-network-policy.yaml#L9-L19", + "sa258c4109d24": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/applicationset-controller/argocd-applicationset-controller-network-policy.yaml#L9-L22", + "sa49c30e332ed": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/notification/argocd-notifications-controller-network-policy.yaml#L9-L20", + "s68b67007da6b": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/settings/settings.go#L1631-L1638", + "s01a52daf078f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/server/argocd-server-deployment.yaml#L121-L126", + "s210b24f04535": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/config/argocd-cmd-params-cm.yaml#L1-L7", + "s0f7f1b3404d9": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/config/argocd-rbac-cm.yaml#L1-L7", + "s85445da68ce7": "https://github.com/argoproj/argo-cd/blob/v3.5.3/common/common.go#L89-L90", + "s357d253fc789": "https://github.com/argoproj/argo-cd/blob/v3.5.3/cmd/argocd-repo-server/commands/argocd_repo_server.go#L257-L260", + "s9df1243dde48": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-application-controller/commands/argocd_application_controller.go#L192-L209", + "s0abbc26d61e4": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd-application-controller/commands/argocd_application_controller.go#L272-L273", + "sfcaf2e03154d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/reposerver/server.go#L49-L85", + "s25a5da2c5e7c": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/reposerver/server.go#L94-L150", + "s73653423b85d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/tls/tls.go#L428-L506", + "s7c7bf274e3ef": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/manifests/base/dex/argocd-dex-server-deployment.yaml#L38-L42", + "s08939ffb906f": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/cmd/argocd/commands/admin/redis_initial_password.go#L22-L89", + "s3fcdf1601776": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L454-L467", + "se987196f20c7": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L539-L547", + "s3cb98aa5348d": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L399-L425", + "s9bfcf07a6bcc": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L314-L346", + "s32323471d785": "https://github.com/argoproj/argo-cd/blob/c9c369efcc5b2a0bd720803f8d14a1c3eaddf579/util/rbac/rbac.go#L35-L42" + } + }, + "flux-oidc": { + "name": "Flux OIDC introduction", + "basis": "v2.9", + "sources": { + "s1ea193b2b3cc": "https://fluxcd.io/blog/2026/06/flux-v2.9.0/" + } + }, + "argo-docs": { + "name": "Argo CD documentation", + "basis": "unknown", + "sources": { + "s51e21b74ece6": "https://argo-cd.readthedocs.io/en/stable/operator-manual/security/", + "s02463a4fb268": "https://argo-cd.readthedocs.io/en/stable/operator-manual/secret-management/", + "s2efb9ee633af": "https://argo-cd.readthedocs.io/en/stable/getting_started/", + "sc2590df86f4e": "https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/", + "sf5f1c41f89bd": "https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/", + "s3767d400e364": "https://argo-cd.readthedocs.io/en/stable/operator-manual/ingress/", + "sdac90731b97d": "https://argo-cd.readthedocs.io/en/stable/operator-manual/webhook/", + "s9357e3b59e84": "https://argo-cd.readthedocs.io/en/stable/user-guide/projects/" + } + }, + "flux": { + "name": "Flux documentation", + "basis": "unknown", + "sources": { + "s493859f3b848": "https://fluxcd.io/flux/security/best-practices/", + "s4dcd744a2444": "https://fluxcd.io/flux/guides/mozilla-sops/", + "sd8618bbbc628": "https://fluxcd.io/flux/components/source/ocirepositories/#secret-reference", + "sf3f80d9941c8": "https://fluxcd.io/flux/installation/configuration/multitenancy/", + "s0669b973542c": "https://fluxcd.io/flux/security/#controller-permissions", + "s13bd7f0204b0": "https://fluxcd.io/flux/installation/bootstrap/github/", + "s8759854c1bdf": "https://fluxcd.io/flux/components/image/imageupdateautomations/", + "s0acf42beed45": "https://fluxcd.io/flux/guides/webhook-receivers/", + "s35bb9a844dcf": "https://fluxcd.io/flux/components/notification/receivers/" + } + }, + "dex": { + "name": "Dex v2.45.1 source", + "basis": "v2.45.1", + "sources": { + "s3b9512b79dea": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/serve.go#L151-L199", + "s46eb2feaa84f": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/serve.go#L553-L572", + "s55b453f266f3": "https://github.com/dexidp/dex/blob/v2.45.1/cmd/dex/config.go#L255-L265" + } + }, + "redis-image": { + "name": "docker-library-redis v8.2.3 source", + "basis": "bcbc692165ce2241e194035041c5b5e01d2e1a5f", + "sources": { + "s67a2f7553c9a": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/Dockerfile#L81-L87", + "s30643c91686d": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/Dockerfile#L151-L154", + "sd04db8453288": "https://github.com/redis/docker-library-redis/blob/bcbc692165ce2241e194035041c5b5e01d2e1a5f/alpine/docker-entrypoint.sh#L93-L111" + } + }, + "redis-source": { + "name": "Redis 8.2.3 source", + "basis": "8.2.3", + "sources": { + "s667fc16bf18f": "https://github.com/redis/redis/blob/8.2.3/src/server.h#L142-L143", + "saa497d27f72f": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2199-L2221", + "s0f96763525b7": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2638-L2673", + "s9b50a7c52721": "https://github.com/redis/redis/blob/8.2.3/src/server.c#L2983-L2996" + } + }, + "curl": { + "name": "curl", + "basis": "curl-8_14_1", + "sources": { + "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md", + "s9537bb790553": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data-binary.md#L4-L32", + "sd3b039bafbf6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data.md#L4-L49" + } + }, + "python": { + "name": "Python documentation", + "basis": "unknown", + "sources": { + "s7a54556e5dac": "https://docs.python.org/3/library/hmac.html", + "s31fccd7d50e5": "https://docs.python.org/3/library/sys.html#sys.stdin" + } + } + }, + "claims": { + "git-boundary": {"text": "Repository writers exercise reconciliation permissions; review platform changes, separate tenants and scope credentials.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s51e21b74ece6", "flux:s493859f3b848"], "status": "REASONED"}, + "secret-storage": {"text": "Protect bootstrap, registry, cluster, webhook and decryption credentials; commit only encrypted secrets and keep SOPS/age keys out of Git.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s02463a4fb268", "flux:s4dcd744a2444", "flux:sd8618bbbc628"], "status": "REASONED"}, + "egress": {"text": "Restrict fetch, API, identity and notification egress; Argo repository allowlists do not constrain remote bases or Helm dependencies.", "components": ["argo-docs", "flux"], "sources": ["argo-docs:s51e21b74ece6", "flux:sf3f80d9941c8"], "status": "REASONED"}, + "argo-bootstrap": {"text": "Rotate the initial admin password before deleting argocd-initial-admin-secret; deletion alone does not rotate it.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:sc2590df86f4e"], "status": "REASONED"}, + "argo-sso": {"text": "The built-in admin is unrestricted; configure named SSO roles and IdP MFA, then set admin.enabled=false.", "components": ["argo-docs"], "sources": ["argo-docs:sc2590df86f4e", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, + "argo-origin": {"text": "Keep the ClusterIP origin private behind authenticated TLS and protect both REST/browser and CLI/gRPC routes.", "components": ["argo-docs", "argo"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:s3767d400e364", "argo:sddf4853ad503"], "status": "REASONED"}, + "argo-server": {"text": "Server defaults to 0.0.0.0:8080; the Service maps 80 and 443 to it.", "components": ["argo"], "sources": ["argo:s6e8c96d23e05", "argo:sddf4853ad503", "argo:sf4dd846c82c4"], "status": "REASONED"}, + "argo-tls": {"text": "Server --insecure/server.insecure defaults false and disables backend TLS, not authentication; use only behind protected TLS termination.", "components": ["argo", "argo-docs"], "sources": ["argo:s6e8c96d23e05", "argo-docs:s3767d400e364"], "status": "REASONED"}, + "argo-client-tls": {"text": "argocd login --insecure skips client certificate verification; the server initially uses a self-signed certificate.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af", "argo-docs:s3767d400e364"], "status": "REASONED"}, + "argo-metrics": {"text": "Server HTTP metrics on 8083 are unauthenticated and bind to --address, not --metrics-address.", "components": ["argo"], "sources": ["argo:s96065cc09776", "argo:s7cc690d1bf69", "argo:s028a4769d6c3", "argo:sf4dd846c82c4", "argo:s6e8c96d23e05"], "status": "REASONED"}, + "argo-profiler": {"text": "Server and repo-server pprof paths return 401 unless the profiler file contains exactly true with no newline; enabled profiling has no caller authentication.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:s028a4769d6c3", "argo:s98b27313f615"], "status": "REASONED"}, + "argo-profiler-file": {"text": "ARGOCD_ENABLE_PROFILER_FILE_PATH defaults to /home/argocd/params/profiler.enabled; the server mounts ConfigMap key server.profile.enabled there.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:sb0648e746fb9", "argo:s2f5af86c62d1"], "status": "REASONED"}, + "repo-grpc": {"text": "Repo-server gRPC binds 0.0.0.0:8081 with TLS by default; mTLS is skipped without the client CA, separate from the serving certificate.", "components": ["argo"], "sources": ["argo:s9bca471ac6d9", "argo:s4bbc848d9923", "argo:sf4dd846c82c4", "argo:s85445da68ce7", "argo:s357d253fc789"], "status": "REASONED"}, + "repo-http": {"text": "Repo-server HTTP metrics and health bind 0.0.0.0:8084 without an authentication wrapper.", "components": ["argo"], "sources": ["argo:s98b27313f615", "argo:s9bca471ac6d9", "argo:sf4dd846c82c4", "argo:s85445da68ce7", "argo:s357d253fc789"], "status": "REASONED"}, + "repo-limits": {"text": "Repo-server requires verified client certificates with a loaded client CA; missing or unstatable serving files trigger a session self-signed certificate, while an invalid present pair errors. No authorization interceptor appears; service method authorization is unverified.", "components": ["argo"], "sources": ["argo:sfcaf2e03154d", "argo:s25a5da2c5e7c", "argo:s73653423b85d"], "status": "REASONED"}, + "controller-metrics": {"text": "Application-controller metrics and health use 8082; the command passes that port to the controller, but the fetched files do not establish the bind address.", "components": ["argo"], "sources": ["argo:sf98b6bc4f5dc", "argo:s9df1243dde48", "argo:s0abbc26d61e4", "argo:sf4dd846c82c4"], "status": "REASONED"}, + "applicationset-webhook": {"text": "ApplicationSet webhook binds :7000; its container and Service declare 7000.", "components": ["argo"], "sources": ["argo:s49a7c83d9371", "argo:s97a588d8a83f"], "status": "REASONED"}, + "applicationset-metrics": {"text": "ApplicationSet metrics bind :8080; its container and Service declare 8080.", "components": ["argo"], "sources": ["argo:s49a7c83d9371", "argo:s97a588d8a83f"], "status": "REASONED"}, + "applicationset-health": {"text": "ApplicationSet health probes bind :8081 without a container or Service port declaration; missing declarations do not close sockets.", "components": ["argo"], "sources": ["argo:s49a7c83d9371"], "status": "REASONED"}, + "notifications": {"text": "Notifications HTTP metrics bind 0.0.0.0:9001 without authentication; a metrics Service and TCP liveness probe exist without containerPort.", "components": ["argo"], "sources": ["argo:s0a85b38dfca8", "argo:s5e141e32ed9c"], "status": "REASONED"}, + "dex-http": {"text": "Dex starts only with nonempty generated configuration; HTTP defaults to TLS on 0.0.0.0:5556 using /tmp/tls.crt and /tmp/tls.key.", "components": ["argo"], "sources": ["argo:s8efcc66f9cb4", "argo:s30c391d9dc23", "argo:se477de2fdfa0"], "status": "REASONED"}, + "dex-grpc": {"text": "Generated Dex gRPC uses 0.0.0.0:5557 without TLS or client-auth settings; Dex v2.45.1 source sets no transport credentials or authentication interceptor for this configuration, while API method authorization is unverified; this assumes the deployed ghcr.io/dexidp/dex:v2.45.1 tag was built from Dex v2.45.1.", "components": ["argo", "dex"], "sources": ["argo:se477de2fdfa0", "argo:s56c41c02ba8b", "argo:s7c7bf274e3ef", "dex:s3b9512b79dea", "dex:s46eb2feaa84f", "dex:s55b453f266f3"], "status": "REASONED"}, + "dex-telemetry": {"text": "Dex telemetry uses HTTP on 0.0.0.0:5558; all three Dex ports have Services.", "components": ["argo"], "sources": ["argo:se477de2fdfa0", "argo:s56c41c02ba8b"], "status": "REASONED"}, + "redis-password": {"text": "Redis receives --requirepass from argocd-redis/auth; secret-init generates and checks that Secret. The redis:8.2.3-alpine tag, taken as built from docker-library-redis v8.2.3, uses Redis wildcard bind defaults and patches protected mode off.", "components": ["argo", "redis-image", "redis-source"], "sources": ["argo:s996c72c5db28", "argo:s08939ffb906f", "redis-image:s67a2f7553c9a", "redis-image:s30643c91686d", "redis-image:sd04db8453288", "redis-source:s667fc16bf18f", "redis-source:saa497d27f72f", "redis-source:s0f96763525b7", "redis-source:s9b50a7c52721"], "status": "REASONED"}, + "redis-tls": {"text": "Base manifests configure no Redis TLS and the Argo Redis client defaults --redis-use-tls=false; protect traffic, Secret and cache.", "components": ["argo"], "sources": ["argo:s996c72c5db28", "argo:s34909d7a1932"], "status": "REASONED"}, + "component-policies": {"text": "Base policies restrict repo gRPC, Redis and Dex callers by component; enforcement needs a supporting CNI.", "components": ["argo"], "sources": ["argo:s1a13cc06db6d", "argo:s4bbf38e83770", "argo:sadccbbcffcd8"], "status": "REASONED"}, + "broad-policies": {"text": "Server policy allows all ingress; other metrics and ApplicationSet webhook allowances admit every namespace. Narrow existing additive allowances and egress separately.", "components": ["argo"], "sources": ["argo:s743c8187cbd7", "argo:s49f63c21c3cd", "argo:sa258c4109d24", "argo:sa49c30e332ed"], "status": "REASONED"}, + "argo-anonymous": {"text": "users.anonymous.enabled defaults off; anonymous callers otherwise inherit policy.default.", "components": ["argo", "argo-docs"], "sources": ["argo:s68b67007da6b", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, + "argo-disable-auth": {"text": "server.disable.auth defaults false; inspect effective args and environment because anonymous-off does not compensate for disabled authentication.", "components": ["argo"], "sources": ["argo:s6e8c96d23e05", "argo:s01a52daf078f", "argo:s210b24f04535"], "status": "REASONED"}, + "argo-default-role": {"text": "Base RBAC ConfigMap omits policy.default; the inspected loader supplies an empty role to the enforcer, which skips its additive default-role check. Default-role grants are additive and cannot be revoked by a later subject rule; define scoped policy.csv roles and group mappings. Server-side wiring remains unverified.", "components": ["argo", "argo-docs"], "sources": ["argo:s0f7f1b3404d9", "argo:s3fcdf1601776", "argo:se987196f20c7", "argo:s3cb98aa5348d", "argo:s9bfcf07a6bcc", "argo:s32323471d785", "argo-docs:sf5f1c41f89bd"], "status": "REASONED"}, + "argo-webhook": {"text": "/api/webhook accepts unauthenticated refresh events without a shared secret; configure one to limit spoofed reconciliation and resource exhaustion.", "components": ["argo-docs"], "sources": ["argo-docs:sdac90731b97d"], "status": "REASONED"}, + "argo-projects": {"text": "Restrict AppProjects including the built-in default project; project constraints do not reduce a compromised controller's Kubernetes credentials.", "components": ["argo-docs"], "sources": ["argo-docs:s9357e3b59e84", "argo-docs:s51e21b74ece6"], "status": "REASONED"}, + "argo-cluster-rbac": {"text": "Reduce controller and registered-cluster argocd-manager write permissions to needed namespaces/resources while retaining required read access.", "components": ["argo-docs"], "sources": ["argo-docs:s51e21b74ece6"], "status": "REASONED"}, + "flux-api": {"text": "Flux has no standalone management login/API; Kubernetes authenticates its custom-resource operations, while separate dashboards need their own controls.", "components": ["flux"], "sources": ["flux:s493859f3b848", "flux:s0669b973542c"], "status": "REASONED"}, + "flux-bootstrap": {"text": "GitHub bootstrap --token-auth retains a PAT in flux-system; --token-auth=false creates a read-only SSH deploy key by default. Protect bootstrap input and Secrets.", "components": ["flux"], "sources": ["flux:s13bd7f0204b0"], "status": "REASONED"}, + "flux-writeback": {"text": "Image automation needs Git write access, including --read-write-key=true for the bootstrap key; scope the repository and protect update branches and objects.", "components": ["flux"], "sources": ["flux:s13bd7f0204b0", "flux:s8759854c1bdf"], "status": "REASONED"}, + "flux-artifacts": {"text": "Source-controller serves fetched artifacts over internal HTTP; enforced NetworkPolicy, not ClusterIP alone, isolates them.", "components": ["flux"], "sources": ["flux:s493859f3b848"], "status": "REASONED"}, + "flux-receiver-port": {"text": "Webhook receiver listens on 9292 behind Service port 80; external exposure needs a route and TLS termination.", "components": ["flux"], "sources": ["flux:s0acf42beed45"], "status": "REASONED"}, + "flux-receiver-types": {"text": "generic validates nothing; generic-hmac, GitHub, Bitbucket and Nexus use HMAC, while gitlab compares X-Gitlab-Token.", "components": ["flux"], "sources": ["flux:s35bb9a844dcf"], "status": "REASONED"}, + "flux-oidc": {"text": "generic-oidc, introduced in Flux 2.9, rejects secretRef and validates bearer tokens; constrain issuer validations and audience, whose default is notification-controller.", "components": ["flux", "flux-oidc"], "sources": ["flux:s35bb9a844dcf", "flux-oidc:s1ea193b2b3cc"], "status": "REASONED"}, + "flux-rbac": {"text": "Only kustomize-controller and helm-controller hold cluster-admin; other controllers still have meaningful permissions including Secret access.", "components": ["flux"], "sources": ["flux:s0669b973542c"], "status": "REASONED"}, + "flux-lockdown": {"text": "Use scoped reconciliation ServiceAccounts, --no-cross-namespace-refs and --no-remote-bases; --default-service-account fills an omission, not an explicit serviceAccountName.", "components": ["flux"], "sources": ["flux:sf3f80d9941c8"], "status": "REASONED"}, + "verify-argo": {"text": "Anonymous application listing should fail with 401 while a valid token lists the expected application; repeat at origin and CLI/gRPC and inspect effective RBAC.", "components": ["argo-docs", "argo", "curl"], "sources": ["argo-docs:sf5f1c41f89bd", "argo:s6e8c96d23e05", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, + "verify-bootstrap": {"text": "Absence of the bootstrap Secret proves removal of that material, not password rotation.", "components": ["argo-docs"], "sources": ["argo-docs:s2efb9ee633af"], "status": "REASONED", "verify": [1]}, + "verify-inventory": {"text": "Inventory Services, routes, host ports, policies and component sockets; node-local ss and policy objects alone do not establish cluster isolation.", "components": ["argo", "flux"], "sources": ["argo:s743c8187cbd7", "flux:s493859f3b848"], "status": "REASONED", "verify": [1]}, + "verify-repo-metrics": {"text": "Base policy permits HTTP 200 metrics to monitoring and unrelated pods; narrowed policy should admit only monitoring, with a successful positive control.", "components": ["argo"], "sources": ["argo:s98b27313f615", "argo:s1a13cc06db6d"], "status": "REASONED", "verify": [2]}, + "verify-server-metrics": {"text": "Repeat on server 8083: only authorized monitoring should receive metrics after narrowing ingress; a failed control is inconclusive.", "components": ["argo"], "sources": ["argo:s028a4769d6c3", "argo:s743c8187cbd7"], "status": "REASONED"}, + "verify-profiler": {"text": "From an allowed pod, pprof cmdline should return 401 disabled, 200 for exact true, then 401 for true plus newline; keep metrics as control.", "components": ["argo"], "sources": ["argo:sc52bbd14bff4", "argo:s028a4769d6c3"], "status": "REASONED"}, + "verify-flux": {"text": "Identical generic-hmac bodies without and with X-Signature should be rejected and accepted with reconciliation; status is type/version dependent. The cited curl curl-8_14_1 lines show --data-binary posting data exactly as specified, preserving newlines and carriage returns that --data strips from @file input, reading @- from stdin and @filename from a file, and posting like --data's POST request.", "components": ["flux", "python", "curl"], "sources": ["flux:s35bb9a844dcf", "python:s7a54556e5dac", "python:s31fccd7d50e5", "curl:s3db522258e44", "curl:s9537bb790553", "curl:sd3b039bafbf6"], "status": "REASONED", "verify": [3]}, + "verify-receiver-status": {"text": "Read receiver name and Ready condition without exposing webhookPath or condition messages; origin checks distinguish receiver rejection from proxy rejection.", "components": ["flux"], "sources": ["flux:s35bb9a844dcf"], "status": "REASONED"} + } +} +```` +====================================================================== +==> self-hosted-ci-runners.md +====================================================================== + +# Self-hosted CI runners: GitHub Actions and GitLab Runner + +Written against GitHub Actions documentation (no recorded version), GitHub REST API 2026-03-10, and Python tomllib minimum 3.11; 3 further sources carry no recorded version. Metadata reviewed 2026-10-12; documentation checked September 2026. The guide records 37 claims: 1 demonstrated by a check that ran and 36 reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + A self-hosted runner exists to execute CI job commands on your own machine, so the trust question is not a bind address but who can cause code to run on it. Both GitHub Actions runners and GitLab Runner are outbound-only for job transport: they poll their service over HTTPS (configure GitLab Runner to verify the @@ -8664,6 +10678,191 @@ Version boundary: GitHub.com documentation checked 18 September 2026, with REST - AWS EC2 instance metadata service (IMDSv2 does not stop authorized in-job requests): https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html - Python tomllib (offline config.toml inspection, Python 3.11+): https://docs.python.org/3.11/library/tomllib.html + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/self-hosted-ci-runners.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- github: GitHub Actions documentation (unknown) +- github-api: GitHub REST API (2026-03-10) +- gitlab: GitLab rolling documentation (unknown) +- docker: Docker Engine documentation (unknown) +- python: Python tomllib minimum (3.11) +- aws: AWS EC2 documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- job-transport (REASONED; github, gitlab): Both runners poll over HTTPS without an inbound job-polling port; verify GitLab TLS and restrict separate autoscaler management or webhook listeners. +- worker-account (REASONED; github, gitlab, docker): Use a dedicated least-privileged account, keep provisioning credentials off workers and confine rootful Docker access to disposable VMs. +- worker-egress (REASONED; github, gitlab, aws): Enforce job egress outside job control, isolate production and other workers, and deny unused metadata over both families; IMDSv2 does not stop arbitrary authorized job code. +- github-eligibility (REASONED; github): Keep self-hosted runners off public repositories; runner groups admit only private repositories by default and the public override should stay off. +- fork-token (REASONED; github): Ordinary fork pull_request jobs normally receive a read-only GITHUB_TOKEN and no other secrets, but can still read host credentials and reachable services. +- privileged-triggers (REASONED; github): pull_request_target and workflow_run can access secrets and write tokens; never execute untrusted PR code, artifacts or caches in these jobs. +- private-forks (REASONED; github): Review private-repository fork settings, which can grant write tokens and secrets; separate trusted privileged workers from untrusted CI. +- registration-token (REASONED; github-api): Registration tokens last one hour; generate on demand, never bake into images, and do not confuse expiry with runner deregistration. +- registration-manager (REASONED; github-api): Keep the App/PAT generating registration or JIT credentials off workers; organization endpoints need Self-hosted runners: write, repository endpoints Administration: write. +- jit (REASONED; github-api): Protect encoded_jit_config and the whole response; JIT runners execute at most one job. +- runner-credential (REASONED; github): Protect the installed runner authentication credential, directory and backups. +- github-job-token (REASONED; github): GITHUB_TOKEN is repository-scoped and per-job; set read-only defaults and grant only trusted jobs additional permissions. +- oidc (REASONED; github): Grant id-token: write only where needed; the cloud trust must restrict issuer, audience and repository/branch or protected-environment subject. +- github-lifecycle (REASONED; github, github-api): Prefer ephemeral/JIT workers and destroy their machines and storage; deregistration does not destroy disks, and labels route rather than authorize jobs. +- shell-executor (REASONED; gitlab): The GitLab shell executor runs as the runner user without isolation; avoid it for untrusted jobs on shared hosts. +- docker-executor (REASONED; gitlab, docker): Use unprivileged Docker; privileged=true or services_privileged and host Docker socket/TCP access can expose the host despite privileged=false. +- executor-host-access (REASONED; gitlab): Deny sensitive host mounts, host PID/network namespaces, unnecessary devices and capabilities; build without the host daemon. +- kubernetes-executor (REASONED; gitlab): Enforce nonprivileged containers, no privilege escalation, scoped ServiceAccounts and no unnecessary token mounting; prevent job overrides. +- gitlab-disposal (REASONED; gitlab): Containers share the kernel; use disposable separate VMs/nodes as needed and one-job autoscaling workers. Instance and Docker Autoscaler GA is recorded as Runner 17.1. +- gitlab-registration (REASONED; gitlab): Legacy registration tokens were disabled by default in GitLab 17.0, with removal scheduled for 20.0; use runner authentication tokens. +- gitlab-runner-token (REASONED; gitlab): Protect and rotate the runner authentication token in config.toml and backups; a cloned runner can steal assigned jobs. +- gitlab-creation (REASONED; gitlab): POST /user/runners needs create_runner scope; keep that access token off workers and set scope/protection at creation. +- gitlab-job-token (REASONED; gitlab): CI_JOB_TOKEN carries supported triggering-user access during the job and appears as CI_REGISTRY_PASSWORD and inside CI_REPOSITORY_URL. +- gitlab-allowlist (REASONED; gitlab): Target-project inbound job-token allowlists limit who may access that target, not job egress; also restrict resource feature visibility. +- gitlab-forks (REASONED; gitlab): Instance runners on public installations accept strangers; project scope does not establish code trust. Review parent-launched fork pipelines or disable ci_allow_fork_pipelines_to_run_in_parent_project. +- gitlab-protected (REASONED; gitlab): Developer access can compromise persistent runners; release runners need ref_protected and a pool separate from fork-merge-request testing. +- gitlab-metrics (REASONED; gitlab): Optional listen_address metrics and profiling have no built-in authorization; 9252 is allocated, not proof of the configured port. +- gitlab-session (REASONED; gitlab): Optional session_server uses TLS and must admit required GitLab traffic at its configured address; 8093 is an example, not an established default. +- verify-eligibility (REASONED; github-api): Fully paginate groups, selected repositories and public repositories' own runners; visibility=all alone is not public eligibility, and inherited access needs review. +- verify-permissions (REASONED; github): Inspect organization and repository workflow-token defaults and PR approval settings; explicit workflow permissions can raise grants and id-token is separate. +- verify-assignment (REASONED; github-api, github): A controlled fork workflow should run when exposed and lose assignment when restricted, while an allowed private job still runs; skipped/pending/offline/timeout is inconclusive. +- verify-config (DEMONSTRATED; python, gitlab): Python 3.14.4 synthetic fixtures reported exposed/fixed Docker, Kubernetes and autoscaler values; wrong types passed through, misplaced keys printed null, and empty/error cases differed in output and exit status. +- verify-daemon (REASONED; gitlab, docker): Resolve docker.host through DOCKER_HOST or the default socket when absent; flag actual job access to a rootful daemon, not tcp:// alone. +- verify-autoscaler (REASONED; gitlab): For Instance/Docker Autoscaler require capacity_per_instance=1 and max_use_count=1 explicitly as integers; absent or other values are findings. +- verify-gitlab-api (REASONED; gitlab): Read inbound allowlists and require release-runner ref_protected against exposed controls; gitlab-runner verify proves authentication only. +- verify-lifecycle (REASONED; github-api, gitlab, github): Check complete deregistration listings and infrastructure disposal; persistent-worker canary must survive in the control, but absence alone does not prove destruction or clean shared caches. +- verify-listeners (REASONED; gitlab): Inventory namespaces and publications, require the permitted-client response, then test disallowed access; any HTTP response proves reachability, failure needs corroborating firewall denial. + +````json +{ + "schema": 1, + "checked": "2026-10-12", + "documentation_checked": "2026-09", + "body_sha256": "72e6c5e6e8aa98cd95989f98eb383a327daaecd20809fcb2d24aea181dee4f4e", + "components": { + "github": { + "name": "GitHub Actions documentation", + "basis": "unknown", + "sources": { + "s215dc07e3642": "https://docs.github.com/en/actions/reference/security/secure-use", + "s92e8e4820392": "https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/manage-access", + "sbbcb15f795a8": "https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows", + "s4561d20e7042": "https://docs.github.com/en/actions/concepts/security/github_token", + "s80fe11d8c90d": "https://docs.github.com/en/actions/reference/security/oidc" + } + }, + "github-api": { + "name": "GitHub REST API", + "basis": "2026-03-10", + "sources": { + "s8217448953a4": "https://docs.github.com/en/rest/actions/self-hosted-runners", + "sd36db9ea1031": "https://docs.github.com/en/rest/actions/self-hosted-runner-groups", + "sbeee4a47110c": "https://docs.github.com/en/rest/actions/workflow-jobs" + } + }, + "gitlab": { + "name": "GitLab rolling documentation", + "basis": "unknown", + "sources": { + "seb30e3937183": "https://docs.gitlab.com/runner/security/", + "sde661ff4762e": "https://docs.gitlab.com/runner/executors/docker_autoscaler/", + "s265254bc5592": "https://docs.gitlab.com/runner/executors/docker/", + "s47b236d63dc4": "https://docs.gitlab.com/runner/configuration/advanced-configuration/", + "s1579c499c75b": "https://docs.gitlab.com/runner/executors/kubernetes/", + "sd2cbff5d5d6e": "https://docs.gitlab.com/ci/runners/new_creation_workflow/", + "se430cbabc011": "https://docs.gitlab.com/api/users/#create-a-runner-linked-to-a-user", + "saef0483b7be7": "https://docs.gitlab.com/ci/jobs/ci_job_token/", + "s699c1c198119": "https://docs.gitlab.com/api/project_job_token_scopes/", + "sbc009ff2866a": "https://docs.gitlab.com/runner/monitoring/" + } + }, + "docker": { + "name": "Docker Engine documentation", + "basis": "unknown", + "sources": { + "s18f59818e451": "https://docs.docker.com/engine/security/" + } + }, + "python": { + "name": "Python tomllib minimum", + "basis": "3.11", + "sources": { + "s37be9e98ea29": "https://docs.python.org/3.11/library/tomllib.html" + } + }, + "aws": { + "name": "AWS EC2 documentation", + "basis": "unknown", + "sources": { + "s82aca706445c": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html" + } + } + }, + "claims": { + "job-transport": {"text": "Both runners poll over HTTPS without an inbound job-polling port; verify GitLab TLS and restrict separate autoscaler management or webhook listeners.", "components": ["github", "gitlab"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "gitlab:sde661ff4762e"], "status": "REASONED"}, + "worker-account": {"text": "Use a dedicated least-privileged account, keep provisioning credentials off workers and confine rootful Docker access to disposable VMs.", "components": ["github", "gitlab", "docker"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "docker:s18f59818e451"], "status": "REASONED"}, + "worker-egress": {"text": "Enforce job egress outside job control, isolate production and other workers, and deny unused metadata over both families; IMDSv2 does not stop arbitrary authorized job code.", "components": ["github", "gitlab", "aws"], "sources": ["github:s215dc07e3642", "gitlab:seb30e3937183", "aws:s82aca706445c"], "status": "REASONED"}, + "github-eligibility": {"text": "Keep self-hosted runners off public repositories; runner groups admit only private repositories by default and the public override should stay off.", "components": ["github"], "sources": ["github:s92e8e4820392", "github:s215dc07e3642"], "status": "REASONED"}, + "fork-token": {"text": "Ordinary fork pull_request jobs normally receive a read-only GITHUB_TOKEN and no other secrets, but can still read host credentials and reachable services.", "components": ["github"], "sources": ["github:s215dc07e3642", "github:sbbcb15f795a8"], "status": "REASONED"}, + "privileged-triggers": {"text": "pull_request_target and workflow_run can access secrets and write tokens; never execute untrusted PR code, artifacts or caches in these jobs.", "components": ["github"], "sources": ["github:sbbcb15f795a8", "github:s215dc07e3642"], "status": "REASONED"}, + "private-forks": {"text": "Review private-repository fork settings, which can grant write tokens and secrets; separate trusted privileged workers from untrusted CI.", "components": ["github"], "sources": ["github:s215dc07e3642"], "status": "REASONED"}, + "registration-token": {"text": "Registration tokens last one hour; generate on demand, never bake into images, and do not confuse expiry with runner deregistration.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, + "registration-manager": {"text": "Keep the App/PAT generating registration or JIT credentials off workers; organization endpoints need Self-hosted runners: write, repository endpoints Administration: write.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, + "jit": {"text": "Protect encoded_jit_config and the whole response; JIT runners execute at most one job.", "components": ["github-api"], "sources": ["github-api:s8217448953a4"], "status": "REASONED"}, + "runner-credential": {"text": "Protect the installed runner authentication credential, directory and backups.", "components": ["github"], "sources": ["github:s215dc07e3642"], "status": "REASONED"}, + "github-job-token": {"text": "GITHUB_TOKEN is repository-scoped and per-job; set read-only defaults and grant only trusted jobs additional permissions.", "components": ["github"], "sources": ["github:s4561d20e7042"], "status": "REASONED"}, + "oidc": {"text": "Grant id-token: write only where needed; the cloud trust must restrict issuer, audience and repository/branch or protected-environment subject.", "components": ["github"], "sources": ["github:s80fe11d8c90d"], "status": "REASONED"}, + "github-lifecycle": {"text": "Prefer ephemeral/JIT workers and destroy their machines and storage; deregistration does not destroy disks, and labels route rather than authorize jobs.", "components": ["github", "github-api"], "sources": ["github:s215dc07e3642", "github:s92e8e4820392", "github-api:s8217448953a4"], "status": "REASONED"}, + "shell-executor": {"text": "The GitLab shell executor runs as the runner user without isolation; avoid it for untrusted jobs on shared hosts.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183"], "status": "REASONED"}, + "docker-executor": {"text": "Use unprivileged Docker; privileged=true or services_privileged and host Docker socket/TCP access can expose the host despite privileged=false.", "components": ["gitlab", "docker"], "sources": ["gitlab:s265254bc5592", "gitlab:seb30e3937183", "docker:s18f59818e451"], "status": "REASONED"}, + "executor-host-access": {"text": "Deny sensitive host mounts, host PID/network namespaces, unnecessary devices and capabilities; build without the host daemon.", "components": ["gitlab"], "sources": ["gitlab:s265254bc5592", "gitlab:s47b236d63dc4"], "status": "REASONED"}, + "kubernetes-executor": {"text": "Enforce nonprivileged containers, no privilege escalation, scoped ServiceAccounts and no unnecessary token mounting; prevent job overrides.", "components": ["gitlab"], "sources": ["gitlab:s1579c499c75b"], "status": "REASONED"}, + "gitlab-disposal": {"text": "Containers share the kernel; use disposable separate VMs/nodes as needed and one-job autoscaling workers. Instance and Docker Autoscaler GA is recorded as Runner 17.1.", "components": ["gitlab"], "sources": ["gitlab:sde661ff4762e", "gitlab:seb30e3937183"], "status": "REASONED"}, + "gitlab-registration": {"text": "Legacy registration tokens were disabled by default in GitLab 17.0, with removal scheduled for 20.0; use runner authentication tokens.", "components": ["gitlab"], "sources": ["gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, + "gitlab-runner-token": {"text": "Protect and rotate the runner authentication token in config.toml and backups; a cloned runner can steal assigned jobs.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, + "gitlab-creation": {"text": "POST /user/runners needs create_runner scope; keep that access token off workers and set scope/protection at creation.", "components": ["gitlab"], "sources": ["gitlab:se430cbabc011", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, + "gitlab-job-token": {"text": "CI_JOB_TOKEN carries supported triggering-user access during the job and appears as CI_REGISTRY_PASSWORD and inside CI_REPOSITORY_URL.", "components": ["gitlab"], "sources": ["gitlab:saef0483b7be7"], "status": "REASONED"}, + "gitlab-allowlist": {"text": "Target-project inbound job-token allowlists limit who may access that target, not job egress; also restrict resource feature visibility.", "components": ["gitlab"], "sources": ["gitlab:saef0483b7be7", "gitlab:s699c1c198119"], "status": "REASONED"}, + "gitlab-forks": {"text": "Instance runners on public installations accept strangers; project scope does not establish code trust. Review parent-launched fork pipelines or disable ci_allow_fork_pipelines_to_run_in_parent_project.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183"], "status": "REASONED"}, + "gitlab-protected": {"text": "Developer access can compromise persistent runners; release runners need ref_protected and a pool separate from fork-merge-request testing.", "components": ["gitlab"], "sources": ["gitlab:seb30e3937183", "gitlab:sd2cbff5d5d6e"], "status": "REASONED"}, + "gitlab-metrics": {"text": "Optional listen_address metrics and profiling have no built-in authorization; 9252 is allocated, not proof of the configured port.", "components": ["gitlab"], "sources": ["gitlab:sbc009ff2866a"], "status": "REASONED"}, + "gitlab-session": {"text": "Optional session_server uses TLS and must admit required GitLab traffic at its configured address; 8093 is an example, not an established default.", "components": ["gitlab"], "sources": ["gitlab:s47b236d63dc4"], "status": "REASONED"}, + "verify-eligibility": {"text": "Fully paginate groups, selected repositories and public repositories' own runners; visibility=all alone is not public eligibility, and inherited access needs review.", "components": ["github-api"], "sources": ["github-api:sd36db9ea1031", "github-api:s8217448953a4"], "status": "REASONED", "verify": [1]}, + "verify-permissions": {"text": "Inspect organization and repository workflow-token defaults and PR approval settings; explicit workflow permissions can raise grants and id-token is separate.", "components": ["github"], "sources": ["github:s4561d20e7042", "github:s80fe11d8c90d"], "status": "REASONED", "verify": [2]}, + "verify-assignment": {"text": "A controlled fork workflow should run when exposed and lose assignment when restricted, while an allowed private job still runs; skipped/pending/offline/timeout is inconclusive.", "components": ["github-api", "github"], "sources": ["github-api:sbeee4a47110c", "github:s92e8e4820392"], "status": "REASONED", "verify": [3]}, + "verify-config": {"text": "Python 3.14.4 synthetic fixtures reported exposed/fixed Docker, Kubernetes and autoscaler values; wrong types passed through, misplaced keys printed null, and empty/error cases differed in output and exit status.", "components": ["python", "gitlab"], "sources": ["python:s37be9e98ea29", "gitlab:s47b236d63dc4", "gitlab:s1579c499c75b"], "status": "DEMONSTRATED", "verify": [4], "evidence": "the extracted Python program ran without sudo on 20 synthetic config.toml cases using Python 3.14.4. Docker fixtures printed true/true, a socket mount and a TCP host versus false/false, empty volumes and a null host; both exited 0. Autoscaler limits printed null/null, 1/2 and 2/1 versus 1/1, all exit 0. Kubernetes fixtures printed true versus false for the three booleans and the supplied service_account. Wrong-type strings, booleans and floats retained their JSON types; misplaced privileged printed docker.privileged null. Empty and runner-less files printed nothing and exited 0. Missing files, malformed TOML and wrong section types exited 1; the second-runner structure error followed one printed runner. Two valid runners printed indices 0 and 1. The runner token key was omitted; a dummy token copied into service_account was printed. These observations cover file inspection only."}, + "verify-daemon": {"text": "Resolve docker.host through DOCKER_HOST or the default socket when absent; flag actual job access to a rootful daemon, not tcp:// alone.", "components": ["gitlab", "docker"], "sources": ["gitlab:s265254bc5592", "docker:s18f59818e451"], "status": "REASONED"}, + "verify-autoscaler": {"text": "For Instance/Docker Autoscaler require capacity_per_instance=1 and max_use_count=1 explicitly as integers; absent or other values are findings.", "components": ["gitlab"], "sources": ["gitlab:sde661ff4762e"], "status": "REASONED"}, + "verify-gitlab-api": {"text": "Read inbound allowlists and require release-runner ref_protected against exposed controls; gitlab-runner verify proves authentication only.", "components": ["gitlab"], "sources": ["gitlab:s699c1c198119", "gitlab:seb30e3937183"], "status": "REASONED", "verify": [5]}, + "verify-lifecycle": {"text": "Check complete deregistration listings and infrastructure disposal; persistent-worker canary must survive in the control, but absence alone does not prove destruction or clean shared caches.", "components": ["github-api", "gitlab", "github"], "sources": ["github-api:s8217448953a4", "gitlab:sde661ff4762e", "github:s215dc07e3642"], "status": "REASONED"}, + "verify-listeners": {"text": "Inventory namespaces and publications, require the permitted-client response, then test disallowed access; any HTTP response proves reachability, failure needs corroborating firewall denial.", "components": ["gitlab"], "sources": ["gitlab:sbc009ff2866a", "gitlab:s47b236d63dc4"], "status": "REASONED", "verify": [6]} + } +} +```` + + ====================================================================== ==> frontend-frameworks.md ====================================================================== @@ -8887,51 +11086,9 @@ REASONED: Host, proxy cookie, redirect and trusted-proxy checks per the cited Sv # Containers: non-root, dropped capabilities, read-only root, and network segmentation - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-18; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): +Written against Docker documentation (no recorded version), Docker Compose source and reference v2.32.4, Kubernetes Pod Security Admission 1.34, kubectl source v1.34.0, Linux process status and capabilities manuals man-pages-6.19, JSON Merge Patch RFC 7396, and BusyBox source 1_36_1; 2 further sources are pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 22 claims, all reasoned from cited sources. -- docker: Docker documentation (unknown) -- docker-daemon: Docker daemon security documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- compose-2324: Docker Compose source and reference (v2.32.4) -- procfs-619: Linux process status and capabilities manuals (man-pages-6.19) -- kubernetes-website: Kubernetes process namespace documentation (47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8) -- kubernetes: Kubernetes documentation (unknown) -- kubernetes-134: Kubernetes Pod Security Admission (1.34) -- kubectl-134: kubectl source (v1.34.0) -- rfc7396: JSON Merge Patch (RFC 7396) -- busybox-1361: BusyBox source (1_36_1) - -Claims (full source mapping in the front matter): - -- docker-user (REASONED; docker): Dockerfile USER sets build/runtime identity; set user and group, since no primary group uses root. Compose user overrides it; unset in both runs as root. -- docker-rootfs (REASONED; docker): Compose read_only makes the root filesystem read-only; provide tmpfs only for required writable paths. -- docker-capabilities (REASONED; docker): cap_drop: [ALL] removes capabilities; add back only specific required capabilities with cap_add. -- docker-escalation (REASONED; docker): no-new-privileges prevents setuid privilege gain; the guide lists bare, equals-true and colon-true spellings as equivalent. -- docker-socket (REASONED; docker, docker-daemon): Never mount /var/run/docker.sock. In Docker's default rootful configuration, daemon control lets a user start a container that can alter the host filesystem without restriction, and docker-group membership grants root-level privileges; Rootless mode runs the daemon and containers without root privileges. Docker documents mounted-socket daemon control with a Docker client, and documents --privileged and host bind mounts; a socket-holding container starting a privileged sibling or bind-mounting host / to escape is this guide's assessment. Rootless mode narrowing the available host privilege but not making the mount safe is this guide's assessment. -- kube-user (REASONED; kubernetes): runAsNonRoot rejects root; pin runAsUser and runAsGroup to 10001, with the guide noting runtime-default GID 0 if unset. -- kube-rootfs (REASONED; kubernetes): Set readOnlyRootFilesystem per container; this is a separate recommendation, not a restricted Pod Security requirement. -- kube-escalation (REASONED; kubernetes): Set allowPrivilegeEscalation: false in the container securityContext. -- kube-capabilities (REASONED; kubernetes): Drop ALL capabilities in the container securityContext. -- kube-seccomp (REASONED; kubernetes): RuntimeDefault selects the runtime syscall filter instead of Unconfined. -- admission (REASONED; kubernetes, kubernetes-134): Namespace enforce: restricted rejects new violating Pods: privileged mode, host namespaces/ports, root, missing capability drops or Unconfined seccomp. -- existing-pods (REASONED; kubernetes, kubernetes-134): Adding the namespace label warns about existing violating Pods without evicting them; recreate workloads. -- policy-semantics (REASONED; kubernetes): NetworkPolicies are additive and require an enforcing CNI; both source egress and destination ingress must allow a connection. -- default-deny (REASONED; kubernetes): The empty podSelector with Ingress and Egress isolates all pods selected in the namespace before narrow allowances. -- dns (REASONED; kubernetes): The app egress rule permits UDP/TCP 53 to all kube-system pods, not just CoreDNS; narrow it to the actual resolver. -- database-path (REASONED; kubernetes): Allow app egress to role: db and db ingress from role: app on TCP 5432; other paths governed by these namespace policies are refused. -- policy-limits (REASONED; kubernetes): hostNetwork and same-node traffic handling are implementation-dependent; database TLS/authentication remain separate controls. -- verify-user (REASONED; docker, compose-2324): Compose exec targets the app service in the resolved project and id must report a nonzero UID; using the actual container name for bare docker exec is this guide's advice. -- verify-rootfs (REASONED; docker, compose-2324): With read_only false, touch on an existing owned path must print WRITABLE; after docker compose up -d --force-recreate with read_only true, it must print blocked. ENOENT or EACCES can also print blocked on a writable filesystem, so blocked alone is not proof of a read-only root. This guide's assessment: the errno, including typical EROFS, depends on the image and path; the probe's exit 0 does not discriminate. -- verify-context (REASONED; kubernetes, kubectl-134, procfs-619, kubernetes-website): kubectl get reads the declared context; kubectl exec inspects the application process for nonzero UID, NoNewPrivs:1, Seccomp:2 and zero effective/bounding capabilities. Clearing CapBnd under drop:[ALL] is this guide's expected runtime state; with shareProcessNamespace, PID 1 belongs to the pod sandbox, so select the application PID. -- verify-probe (REASONED; kubernetes, kubectl-134, rfc7396, busybox-1361): Resolve the db ClusterIP once; keep matching container names and restricted-compliant overrides for busybox:1.36 probes. Pinned kubectl and BusyBox sources explain merge and nc flags; the busybox:1.36 image's nc build and options remain unverified. -- verify-segmentation (REASONED; kubernetes): The role: app TCP probe must succeed first; role: other to the same IP must fail at TCP, not DNS. This tests combined segmentation, not either policy layer independently. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. A model server, chat UI, or proxy is the thing most likely to face untrusted input, so treat its container as compromised eventually and limit what that gets an attacker: not root, not the host's capabilities, not a writable filesystem, and not a route to the rest of the network. @@ -9123,6 +11280,197 @@ kubectl run probe-forbidden --rm -it --restart=Never --image=busybox:1.36 --labe - kubectl v1.34.0 exec Pod and container examples (checked October 2026): https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/exec/exec.go#L52-L56 - Kubernetes website 47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8 shared process namespace and pod sandbox (checked October 2026): https://github.com/kubernetes/website/blob/47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8/content/en/docs/tasks/configure-pod-container/share-process-namespace.md#L115-L119 + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/container-hardening.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- docker: Docker documentation (unknown) +- docker-daemon: Docker daemon security documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- compose-2324: Docker Compose source and reference (v2.32.4) +- kubernetes-website: Kubernetes process namespace documentation (47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8) +- kubernetes: Kubernetes documentation (unknown) +- kubernetes-134: Kubernetes Pod Security Admission (1.34) +- kubectl-134: kubectl source (v1.34.0) +- procfs-619: Linux process status and capabilities manuals (man-pages-6.19) +- rfc7396: JSON Merge Patch (RFC 7396) +- busybox-1361: BusyBox source (1_36_1) + +Claims (full source mapping in the JSON block below): + +- docker-user (REASONED; docker): Dockerfile USER sets build/runtime identity; set user and group, since no primary group uses root. Compose user overrides it; unset in both runs as root. +- docker-rootfs (REASONED; docker): Compose read_only makes the root filesystem read-only; provide tmpfs only for required writable paths. +- docker-capabilities (REASONED; docker): cap_drop: [ALL] removes capabilities; add back only specific required capabilities with cap_add. +- docker-escalation (REASONED; docker): no-new-privileges prevents setuid privilege gain; the guide lists bare, equals-true and colon-true spellings as equivalent. +- docker-socket (REASONED; docker, docker-daemon): Never mount /var/run/docker.sock. In Docker's default rootful configuration, daemon control lets a user start a container that can alter the host filesystem without restriction, and docker-group membership grants root-level privileges; Rootless mode runs the daemon and containers without root privileges. Docker documents mounted-socket daemon control with a Docker client, and documents --privileged and host bind mounts; a socket-holding container starting a privileged sibling or bind-mounting host / to escape is this guide's assessment. Rootless mode narrowing the available host privilege but not making the mount safe is this guide's assessment. +- kube-user (REASONED; kubernetes): runAsNonRoot rejects root; pin runAsUser and runAsGroup to 10001, with the guide noting runtime-default GID 0 if unset. +- kube-rootfs (REASONED; kubernetes): Set readOnlyRootFilesystem per container; this is a separate recommendation, not a restricted Pod Security requirement. +- kube-escalation (REASONED; kubernetes): Set allowPrivilegeEscalation: false in the container securityContext. +- kube-capabilities (REASONED; kubernetes): Drop ALL capabilities in the container securityContext. +- kube-seccomp (REASONED; kubernetes): RuntimeDefault selects the runtime syscall filter instead of Unconfined. +- admission (REASONED; kubernetes, kubernetes-134): Namespace enforce: restricted rejects new violating Pods: privileged mode, host namespaces/ports, root, missing capability drops or Unconfined seccomp. +- existing-pods (REASONED; kubernetes, kubernetes-134): Adding the namespace label warns about existing violating Pods without evicting them; recreate workloads. +- policy-semantics (REASONED; kubernetes): NetworkPolicies are additive and require an enforcing CNI; both source egress and destination ingress must allow a connection. +- default-deny (REASONED; kubernetes): The empty podSelector with Ingress and Egress isolates all pods selected in the namespace before narrow allowances. +- dns (REASONED; kubernetes): The app egress rule permits UDP/TCP 53 to all kube-system pods, not just CoreDNS; narrow it to the actual resolver. +- database-path (REASONED; kubernetes): Allow app egress to role: db and db ingress from role: app on TCP 5432; other paths governed by these namespace policies are refused. +- policy-limits (REASONED; kubernetes): hostNetwork and same-node traffic handling are implementation-dependent; database TLS/authentication remain separate controls. +- verify-user (REASONED; docker, compose-2324): Compose exec targets the app service in the resolved project and id must report a nonzero UID; using the actual container name for bare docker exec is this guide's advice. +- verify-rootfs (REASONED; docker, compose-2324): With read_only false, touch on an existing owned path must print WRITABLE; after docker compose up -d --force-recreate with read_only true, it must print blocked. ENOENT or EACCES can also print blocked on a writable filesystem, so blocked alone is not proof of a read-only root. This guide's assessment: the errno, including typical EROFS, depends on the image and path; the probe's exit 0 does not discriminate. +- verify-context (REASONED; kubernetes, kubectl-134, procfs-619, kubernetes-website): kubectl get reads the declared context; kubectl exec inspects the application process for nonzero UID, NoNewPrivs:1, Seccomp:2 and zero effective/bounding capabilities. Clearing CapBnd under drop:[ALL] is this guide's expected runtime state; with shareProcessNamespace, PID 1 belongs to the pod sandbox, so select the application PID. +- verify-probe (REASONED; kubernetes, kubectl-134, rfc7396, busybox-1361): Resolve the db ClusterIP once; keep matching container names and restricted-compliant overrides for busybox:1.36 probes. Pinned kubectl and BusyBox sources explain merge and nc flags; the busybox:1.36 image's nc build and options remain unverified. +- verify-segmentation (REASONED; kubernetes): The role: app TCP probe must succeed first; role: other to the same IP must fail at TCP, not DNS. This tests combined segmentation, not either policy layer independently. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "096aac84d8e85e741360a95b6e4fbadf6b3621a153725920785f898bb479528b", + "components": { + "docker": { + "name": "Docker documentation", + "basis": "unknown", + "sources": { + "s6ca35cbd69e7": "https://docs.docker.com/reference/dockerfile/", + "s8b7485a246e4": "https://docs.docker.com/reference/compose-file/", + "s5d6294cf605e": "https://docs.docker.com/reference/cli/docker/container/run/" + } + }, + "docker-daemon": { + "name": "Docker daemon security documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "secebea1a69ab": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/_index.md#L84-L112", + "s79490cd2f3b2": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/install/linux-postinstall.md#L29-L39", + "s8eab393a61c6": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/security/rootless/_index.md#L8-L21", + "s94d57fcbfafe": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1330-L1350", + "s372ddf9da1f0": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1460-L1467", + "see4d326cd914": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/data/cli/engine/docker_container_run.yaml#L1495-L1515" + } + }, + "compose-2324": { + "name": "Docker Compose source and reference", + "basis": "v2.32.4", + "sources": { + "s906b5822e9c4": "https://github.com/docker/compose/blob/v2.32.4/docs/reference/compose_exec.md#L1-L7", + "sfe4c568a9887": "https://github.com/docker/compose/blob/v2.32.4/cmd/compose/exec.go#L84-L110", + "s1bf114813e47": "https://github.com/docker/compose/blob/v2.32.4/cmd/compose/up.go#L145-L155" + } + }, + "kubernetes-website": { + "name": "Kubernetes process namespace documentation", + "basis": "47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8", + "sources": { + "s3df8336a2aa8": "https://github.com/kubernetes/website/blob/47132eb5367cfdcba0a3b33a0b3ffc81185bd7e8/content/en/docs/tasks/configure-pod-container/share-process-namespace.md#L115-L119" + } + }, + "kubernetes": { + "name": "Kubernetes documentation", + "basis": "unknown", + "sources": { + "sb78ea91d0308": "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/", + "s1a2d717974c7": "https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "s953450b4076f": "https://kubernetes.io/docs/concepts/services-networking/network-policies/" + } + }, + "kubernetes-134": { + "name": "Kubernetes Pod Security Admission", + "basis": "1.34", + "sources": { + "s9b9aaf5067e2": "https://v1-34.docs.kubernetes.io/docs/concepts/security/pod-security-admission/", + "sa25682f200f0": "https://v1-34.docs.kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/" + } + }, + "kubectl-134": { + "name": "kubectl source", + "basis": "v1.34.0", + "sources": { + "s0906dc78482b": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L293-L310", + "s1adb8cde69c0": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L103-L194", + "sc98b92ba3ebf": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go#L591-L622", + "s2ffc04de1d63": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/util/override_options.go#L29-L90", + "sf5417ce59241": "https://github.com/kubernetes/kubernetes/blob/v1.34.0/staging/src/k8s.io/kubectl/pkg/cmd/exec/exec.go#L52-L56" + } + }, + "procfs-619": { + "name": "Linux process status and capabilities manuals", + "basis": "man-pages-6.19", + "sources": { + "sa94440d59945": "https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man/man5/proc_pid_status.5?h=man-pages-6.19#n315", + "s74035196d5f7": "https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man/man7/capabilities.7?h=man-pages-6.19#n1367" + } + }, + "rfc7396": { + "name": "JSON Merge Patch", + "basis": "RFC 7396", + "sources": { + "sda39f95d0890": "https://www.rfc-editor.org/rfc/rfc7396#section-2" + } + }, + "busybox-1361": { + "name": "BusyBox source", + "basis": "1_36_1", + "sources": { + "s14b89daa008d": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc.c#L30-L59", + "s34bdee292f79": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc_bloaty.c#L61-L93", + "s8a53fb647996": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc_bloaty.c#L888-L922" + } + } + }, + "claims": { + "docker-user": {"text": "Dockerfile USER sets build/runtime identity; set user and group, since no primary group uses root. Compose user overrides it; unset in both runs as root.", "components": ["docker"], "sources": ["docker:s6ca35cbd69e7", "docker:s8b7485a246e4"], "status": "REASONED"}, + "docker-rootfs": {"text": "Compose read_only makes the root filesystem read-only; provide tmpfs only for required writable paths.", "components": ["docker"], "sources": ["docker:s8b7485a246e4"], "status": "REASONED"}, + "docker-capabilities": {"text": "cap_drop: [ALL] removes capabilities; add back only specific required capabilities with cap_add.", "components": ["docker"], "sources": ["docker:s8b7485a246e4"], "status": "REASONED"}, + "docker-escalation": {"text": "no-new-privileges prevents setuid privilege gain; the guide lists bare, equals-true and colon-true spellings as equivalent.", "components": ["docker"], "sources": ["docker:s5d6294cf605e", "docker:s8b7485a246e4"], "status": "REASONED"}, + "docker-socket": {"text": "Never mount /var/run/docker.sock. In Docker's default rootful configuration, daemon control lets a user start a container that can alter the host filesystem without restriction, and docker-group membership grants root-level privileges; Rootless mode runs the daemon and containers without root privileges. Docker documents mounted-socket daemon control with a Docker client, and documents --privileged and host bind mounts; a socket-holding container starting a privileged sibling or bind-mounting host / to escape is this guide's assessment. Rootless mode narrowing the available host privilege but not making the mount safe is this guide's assessment.", "components": ["docker", "docker-daemon"], "sources": ["docker:s5d6294cf605e", "docker-daemon:secebea1a69ab", "docker-daemon:s79490cd2f3b2", "docker-daemon:s8eab393a61c6", "docker-daemon:s94d57fcbfafe", "docker-daemon:s372ddf9da1f0", "docker-daemon:see4d326cd914"], "status": "REASONED"}, + "kube-user": {"text": "runAsNonRoot rejects root; pin runAsUser and runAsGroup to 10001, with the guide noting runtime-default GID 0 if unset.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308"], "status": "REASONED"}, + "kube-rootfs": {"text": "Set readOnlyRootFilesystem per container; this is a separate recommendation, not a restricted Pod Security requirement.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, + "kube-escalation": {"text": "Set allowPrivilegeEscalation: false in the container securityContext.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, + "kube-capabilities": {"text": "Drop ALL capabilities in the container securityContext.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, + "kube-seccomp": {"text": "RuntimeDefault selects the runtime syscall filter instead of Unconfined.", "components": ["kubernetes"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7"], "status": "REASONED"}, + "admission": {"text": "Namespace enforce: restricted rejects new violating Pods: privileged mode, host namespaces/ports, root, missing capability drops or Unconfined seccomp.", "components": ["kubernetes", "kubernetes-134"], "sources": ["kubernetes:s1a2d717974c7", "kubernetes-134:s9b9aaf5067e2"], "status": "REASONED"}, + "existing-pods": {"text": "Adding the namespace label warns about existing violating Pods without evicting them; recreate workloads.", "components": ["kubernetes", "kubernetes-134"], "sources": ["kubernetes:s1a2d717974c7", "kubernetes-134:sa25682f200f0"], "status": "REASONED"}, + "policy-semantics": {"text": "NetworkPolicies are additive and require an enforcing CNI; both source egress and destination ingress must allow a connection.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "default-deny": {"text": "The empty podSelector with Ingress and Egress isolates all pods selected in the namespace before narrow allowances.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "dns": {"text": "The app egress rule permits UDP/TCP 53 to all kube-system pods, not just CoreDNS; narrow it to the actual resolver.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "database-path": {"text": "Allow app egress to role: db and db ingress from role: app on TCP 5432; other paths governed by these namespace policies are refused.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "policy-limits": {"text": "hostNetwork and same-node traffic handling are implementation-dependent; database TLS/authentication remain separate controls.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED"}, + "verify-user": {"text": "Compose exec targets the app service in the resolved project and id must report a nonzero UID; using the actual container name for bare docker exec is this guide's advice.", "components": ["docker", "compose-2324"], "sources": ["docker:s6ca35cbd69e7", "docker:s8b7485a246e4", "compose-2324:s906b5822e9c4", "compose-2324:sfe4c568a9887"], "status": "REASONED", "verify": [1]}, + "verify-rootfs": {"text": "With read_only false, touch on an existing owned path must print WRITABLE; after docker compose up -d --force-recreate with read_only true, it must print blocked. ENOENT or EACCES can also print blocked on a writable filesystem, so blocked alone is not proof of a read-only root. This guide's assessment: the errno, including typical EROFS, depends on the image and path; the probe's exit 0 does not discriminate.", "components": ["docker", "compose-2324"], "sources": ["docker:s8b7485a246e4", "compose-2324:s1bf114813e47"], "status": "REASONED", "verify": [1]}, + "verify-context": {"text": "kubectl get reads the declared context; kubectl exec inspects the application process for nonzero UID, NoNewPrivs:1, Seccomp:2 and zero effective/bounding capabilities. Clearing CapBnd under drop:[ALL] is this guide's expected runtime state; with shareProcessNamespace, PID 1 belongs to the pod sandbox, so select the application PID.", "components": ["kubernetes", "kubectl-134", "procfs-619", "kubernetes-website"], "sources": ["kubernetes:sb78ea91d0308", "kubectl-134:sf5417ce59241", "procfs-619:sa94440d59945", "procfs-619:s74035196d5f7", "kubernetes-website:s3df8336a2aa8"], "status": "REASONED", "verify": [1]}, + "verify-probe": {"text": "Resolve the db ClusterIP once; keep matching container names and restricted-compliant overrides for busybox:1.36 probes. Pinned kubectl and BusyBox sources explain merge and nc flags; the busybox:1.36 image's nc build and options remain unverified.", "components": ["kubernetes", "kubectl-134", "rfc7396", "busybox-1361"], "sources": ["kubernetes:sb78ea91d0308", "kubernetes:s1a2d717974c7", "kubernetes:s953450b4076f", "kubectl-134:s0906dc78482b", "kubectl-134:s1adb8cde69c0", "kubectl-134:sc98b92ba3ebf", "kubectl-134:s2ffc04de1d63", "rfc7396:sda39f95d0890", "busybox-1361:s14b89daa008d", "busybox-1361:s34bdee292f79", "busybox-1361:s8a53fb647996"], "status": "REASONED", "verify": [1]}, + "verify-segmentation": {"text": "The role: app TCP probe must succeed first; role: other to the same IP must fail at TCP, not DNS. This tests combined segmentation, not either policy layer independently.", "components": ["kubernetes"], "sources": ["kubernetes:s953450b4076f"], "status": "REASONED", "verify": [1]} + } +} +```` + + ====================================================================== ==> nextjs.md ====================================================================== @@ -36677,78 +39025,9 @@ Claims (full source mapping in the JSON block below): # Ray: dashboard, Jobs, and Client ports execute code - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-09-26; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +Written against Ray pinned source ray-2.58.0, Ray token authentication introduction 2.52.0, KubeRay chart v1.7.0, KubeRay authentication source v1.6.0, KubeRay prior RayCluster types v1.5.0, CPython WSGI server v3.11.0, and curl minimum write-out version 7.75.0; 1 further source is pinned to a commit and 5 carry no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 46 claims: 1 demonstrated by a check that ran and 45 reasoned from cited sources. -Components (recorded basis): - -- ray: Ray pinned source (ray-2.58.0) -- docs: Ray documentation (unknown) -- token-min: Ray token authentication introduction (2.52.0) -- redis-history: Ray Redis history announcement (unknown) -- docker: Docker publishing documentation (unknown) -- kuberay-auth: KubeRay authentication documentation (unknown) -- kuberay: KubeRay chart (v1.7.0) -- kubernetes: Kubernetes security contexts (unknown) -- python: CPython WSGI server (v3.11.0) -- curl: curl minimum write-out version (7.75.0) -- docker-release: Docker Engine release notes (4e9a5751518ed8223a8dcde53693badddd72604f) -- kuberay-auth-source: KubeRay authentication source (v1.6.0) -- kuberay-auth-prior: KubeRay prior RayCluster types (v1.5.0) - -Claims (full source mapping in the front matter): - -- boundary (REASONED; docs): Dashboard, Jobs and Client access permits arbitrary code execution; use an isolated cluster network and separate clusters for mutually untrusted jobs. -- dashboard (REASONED; ray): Dashboard defaults to resolved localhost, IPv4 before IPv6 then 127.0.0.1, on port 8265; state loopback explicitly. -- docker (REASONED; docker, docker-release): Bridge publishing uses host loopback and container-interface bind; this guide assesses Docker 28.0.0 or later with normal NAT as the loopback-isolation minimum because the 28.0.0 release notes record a fix for neighbour hosts reaching loopback-mapped ports. -- access (REASONED; docs, kuberay-auth): Reach Dashboard/Jobs through loopback SSH forwarding, launcher forwarding, KubeRay forwarding or a private tailnet; public browser access needs an authenticating TLS proxy. -- head-ports (REASONED; docs): Head GCS uses 6379 and Client 10001; worker ports default 10002-19999 plus randomized listeners. -- core-bind (REASONED; ray): Core gRPC source binds all interfaces unless node address is exactly 127.0.0.1, ::1 or localhost; private node IP does not imply private bind. -- gcs-observation (DEMONSTRATED; ray): Four attempted Ray 2.58.0 loopback starts still showed GCS *:6379; the cause was not established and the watcher stopped the runs. -- client-bind (REASONED; ray): Ray Client server/proxier listen on the supplied node address plus loopback; restrict access and prefer Jobs over publishing Client. -- serve-replica (REASONED; ray): Serve inter-deployment gRPC binds [::] on an OS-chosen port; this is source reasoning, not a run. -- agents (REASONED; ray): --include-dashboard=false does not stop dashboard/runtime-env agents; head dashboard itself is reduced to usage stats. -- minimal (REASONED; ray): Missing optional imports select minimal mode: no agent HTTP/gRPC or metrics reporter; runtime-env HTTP remains. Base installs may have full dependencies. -- agent-http (REASONED; ray): Non-minimal agent HTTP defaults 52365, including ray.init; --dashboard-agent-listen-port changes it, with node-IP plus conditional localhost binds. -- agent-jobs (REASONED; ray): Direct /api/job_agent/jobs/ submission and stop/delete/log routes bypass a proxy on 8265; head forwarding reaches its agent job manager. -- agent-placement (REASONED; ray): Worker agents accept cluster submissions with their own managers; drivers normally use the head unless resources, labels or worker-placement override permit workers. -- agent-logs (REASONED; ray): Agent static /logs exposes the node log directory and index. -- agent-http-auth (REASONED; ray): Without token mode a browser heuristic is the only filter; token mode guards all agent HTTP paths except two health endpoints. -- agent-grpc (REASONED; ray): Agent gRPC defaults OS-assigned with --dashboard-agent-grpc-port override; localhost or wildcard bind serves log/profiling RPCs with optional token interception. -- runtime-agent (REASONED; ray): Runtime-env HTTP defaults OS-assigned on node IP with --runtime-env-agent-port override; create/delete installs packages, and token middleware exempts no route. -- metrics-agent (REASONED; ray): Metrics default enabled; non-minimal reporter uses an OS-assigned port or --metrics-export-port, outside all token middleware. -- metrics-ipv4 (REASONED; ray, python): WSGI metrics uses AF_INET: empty address is IPv4 wildcard, localhost is used for local node addresses, and resolved ::1 cannot bind this socket. -- node-normalization (REASONED; ray): Both CLI and ray.init normalize localhost node arguments to a discovered address; cluster-mode setting changes whole-node selection, not independent agent binds. -- agent-family (REASONED; ray): Agent gRPC wildcard selection is 0.0.0.0 or :: based on resolved localhost, independently of node address family. -- gcs-not-redis (REASONED; redis-history, docs): 6379 is GCS, not Redis; Redis stopped being launched by default in 1.11. -- redis-backend (REASONED; docs): External Redis is opt-in GCS fault-tolerance storage; Redis credentials do not authenticate GCS clients and RAY_REDIS_PASSWORD is not a head flag substitute. -- token-default (REASONED; token-min, ray): Shared token authentication exists since 2.52.0 but is disabled by default as of 2.58.0; it supplements network isolation and does not protect Serve apps. -- token-input (REASONED; ray): Enable RAY_AUTH_MODE=token; precedence is RAY_AUTH_TOKEN, RAY_AUTH_TOKEN_PATH then ~/.ray/auth_token, shared by every node/client. -- token-storage (REASONED; ray): Generate/protect token files and copy before startup; tokens are plaintext, do not expire and must never be committed. -- token-transport (REASONED; docs, ray): Token HTTP headers require a trusted tunnel or TLS; plaintext HTTP exposes them on the network. -- kuberay-token (REASONED; kuberay-auth, kuberay-auth-source, kuberay-auth-prior): KubeRay v1.6.0 defines authOptions; v1.5.0 RayClusterSpec lacks it. With token mode enabled, the controller creates a random-token Secret when Kubernetes ServiceAccount token authentication is off, SecretName is null or empty, and the generated-name Secret does not already exist; it leaves an existing generated-name Secret or a supplied named Secret in place and creates no Secret in ServiceAccount mode. The pod builder sets RAY_AUTH_MODE when unset on head and worker Ray containers, on the wait-gcs-ready init container when present, and on the head autoscaler when autoscaling is enabled; outside ServiceAccount mode it adds RAY_AUTH_TOKEN from the selected Secret when unset, and in ServiceAccount mode it does not add RAY_AUTH_TOKEN. This guide infers support in v1.6.0 and later from the compared tags; later releases have not been checked. -- mfa (REASONED; docs): Ray has no user accounts; MFA belongs on SSH, tailnet or identity-aware proxy access. -- grpc-tls (REASONED; docs): RAY_USE_TLS defaults 0; export it and server certificate/key/CA variables on every node before startup for internal gRPC mutual TLS. -- tls-scope (REASONED; docs): gRPC TLS can cost performance and does not replace isolation or the dashboard/Jobs HTTP TLS proxy. -- runtime-code (REASONED; docs): Job runtime_env installs before entrypoint; ray.init environments apply to child tasks/actors. Packages, archives, py_executable and setup hooks execute submitted code. -- runtime-policy (REASONED; docs): No built-in package/URI/hook admission policy; eager_install and setup_timeout_seconds control timing, not trust. Prebuild reviewed dependencies. -- serve-bind (REASONED; docs): Serve HTTP defaults port 8000, Python host 127.0.0.1 but config-file host 0.0.0.0; EveryNode puts proxies on nodes with replicas. -- serve-auth (REASONED; docs): Serve apps have no built-in application auth and are outside cluster token mode; restrict origins and add proxy or FastAPI authentication. -- serve-tls (REASONED; docs): Serve ssl_keyfile/ssl_certfile/ssl_ca_certs default None; supplying a CA does not itself require client certificates. -- serve-grpc (REASONED; docs, ray): Optional Serve gRPC uses 9000 only with configured servicer functions. -- image-privilege (REASONED; ray): Official image ray UID 1000/GID 100 has passwordless sudo; non-root startup alone does not prevent root escalation. -- pod-security (REASONED; kuberay, kubernetes): KubeRay v1.7.0 head/worker pod/container contexts default empty; enforce non-root UID/GID, no privilege escalation, dropped capabilities and RuntimeDefault seccomp. -- verify-inventory (REASONED; ray): Inventory every head/worker and actual randomized agent ports after restarts; expected missing listeners need explanation. No isolated multi-node cluster was available. -- verify-external (REASONED; docs, curl): Probe every routed IPv4/IPv6 origin and inventoried port; completed TCP, even HTTP 401 or non-HTTP, proves exposure. Resolver/local errors are inconclusive. -- verify-jobs (REASONED; ray, kuberay-auth): Through the trusted tunnel valid-token submission must succeed and token-free submission must fail authentication with 401, not a connection error. -- verify-grpc-tls (REASONED; docs): ray health-check must pass with the correct CA and fail certificate verification with a wrong CA/name; local config errors are inconclusive. -- verify-agent (REASONED; ray): Direct non-browser /logs/ must expose an index with token mode off, allow a valid token with it on and reject absent tokens with 401; health exemptions cannot discriminate. -- verify-token-secret (REASONED; ray, curl): Trusted-path hidden prompt and stdin header avoid new token argv/history exposure and set +x avoids tracing; account owner/root can still read memory. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Ray's own security page is blunt: if you expose the Ray Dashboard, Ray Jobs, or Ray Client services, "anybody who can access the associated ports can execute arbitrary code on your Ray Cluster", explicitly by submitting a Job or connecting a Client, indirectly through the Dashboard REST API, and implicitly because Ray deserializes arbitrary Python objects with cloudpickle. Ray "doesn't implement access controls for developers interacting with a given cluster"; security and isolation "must be enforced outside of the Ray Cluster". The head ports in question are the dashboard (and Jobs API) on `8265`, the Ray Client server on `10001`, and the head node port `6379`, all plain HTTP or gRPC with no login of their own unless you enable token authentication (step 3). The per-node agent listeners in step 2 add independent paths to job submission, logs, profiling, runtime environments and metrics. @@ -37046,6 +39325,331 @@ REASONED: mixed historical listener observations and unrun service comparisons f - KubeRay ray-cluster Helm chart values (head/worker podSecurityContext and securityContext, default {}): https://github.com/ray-project/kuberay/blob/v1.7.0/helm-chart/ray-cluster/values.yaml - [Ray ray-2.58.0 Serve gRPC default port and servicer-function requirement (checked October 2026)](https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/serve/config.py#L955-L963). + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/ray.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- ray: Ray pinned source (ray-2.58.0) +- token-min: Ray token authentication introduction (2.52.0) +- docs: Ray documentation (unknown) +- redis-history: Ray Redis history announcement (unknown) +- kuberay-auth: KubeRay authentication documentation (unknown) +- kuberay: KubeRay chart (v1.7.0) +- kuberay-auth-source: KubeRay authentication source (v1.6.0) +- kuberay-auth-prior: KubeRay prior RayCluster types (v1.5.0) +- docker: Docker publishing documentation (unknown) +- kubernetes: Kubernetes security contexts (unknown) +- docker-release: Docker Engine release notes (4e9a5751518ed8223a8dcde53693badddd72604f) +- python: CPython WSGI server (v3.11.0) +- curl: curl minimum write-out version (7.75.0) + +Claims (full source mapping in the JSON block below): + +- boundary (REASONED; docs): Dashboard, Jobs and Client access permits arbitrary code execution; use an isolated cluster network and separate clusters for mutually untrusted jobs. +- dashboard (REASONED; ray): Dashboard defaults to resolved localhost, IPv4 before IPv6 then 127.0.0.1, on port 8265; state loopback explicitly. +- docker (REASONED; docker, docker-release): Bridge publishing uses host loopback and container-interface bind; this guide assesses Docker 28.0.0 or later with normal NAT as the loopback-isolation minimum because the 28.0.0 release notes record a fix for neighbour hosts reaching loopback-mapped ports. +- access (REASONED; docs, kuberay-auth): Reach Dashboard/Jobs through loopback SSH forwarding, launcher forwarding, KubeRay forwarding or a private tailnet; public browser access needs an authenticating TLS proxy. +- head-ports (REASONED; docs): Head GCS uses 6379 and Client 10001; worker ports default 10002-19999 plus randomized listeners. +- core-bind (REASONED; ray): Core gRPC source binds all interfaces unless node address is exactly 127.0.0.1, ::1 or localhost; private node IP does not imply private bind. +- gcs-observation (DEMONSTRATED; ray): Four attempted Ray 2.58.0 loopback starts still showed GCS *:6379; the cause was not established and the watcher stopped the runs. +- client-bind (REASONED; ray): Ray Client server/proxier listen on the supplied node address plus loopback; restrict access and prefer Jobs over publishing Client. +- serve-replica (REASONED; ray): Serve inter-deployment gRPC binds [::] on an OS-chosen port; this is source reasoning, not a run. +- agents (REASONED; ray): --include-dashboard=false does not stop dashboard/runtime-env agents; head dashboard itself is reduced to usage stats. +- minimal (REASONED; ray): Missing optional imports select minimal mode: no agent HTTP/gRPC or metrics reporter; runtime-env HTTP remains. Base installs may have full dependencies. +- agent-http (REASONED; ray): Non-minimal agent HTTP defaults 52365, including ray.init; --dashboard-agent-listen-port changes it, with node-IP plus conditional localhost binds. +- agent-jobs (REASONED; ray): Direct /api/job_agent/jobs/ submission and stop/delete/log routes bypass a proxy on 8265; head forwarding reaches its agent job manager. +- agent-placement (REASONED; ray): Worker agents accept cluster submissions with their own managers; drivers normally use the head unless resources, labels or worker-placement override permit workers. +- agent-logs (REASONED; ray): Agent static /logs exposes the node log directory and index. +- agent-http-auth (REASONED; ray): Without token mode a browser heuristic is the only filter; token mode guards all agent HTTP paths except two health endpoints. +- agent-grpc (REASONED; ray): Agent gRPC defaults OS-assigned with --dashboard-agent-grpc-port override; localhost or wildcard bind serves log/profiling RPCs with optional token interception. +- runtime-agent (REASONED; ray): Runtime-env HTTP defaults OS-assigned on node IP with --runtime-env-agent-port override; create/delete installs packages, and token middleware exempts no route. +- metrics-agent (REASONED; ray): Metrics default enabled; non-minimal reporter uses an OS-assigned port or --metrics-export-port, outside all token middleware. +- metrics-ipv4 (REASONED; ray, python): WSGI metrics uses AF_INET: empty address is IPv4 wildcard, localhost is used for local node addresses, and resolved ::1 cannot bind this socket. +- node-normalization (REASONED; ray): Both CLI and ray.init normalize localhost node arguments to a discovered address; cluster-mode setting changes whole-node selection, not independent agent binds. +- agent-family (REASONED; ray): Agent gRPC wildcard selection is 0.0.0.0 or :: based on resolved localhost, independently of node address family. +- gcs-not-redis (REASONED; redis-history, docs): 6379 is GCS, not Redis; Redis stopped being launched by default in 1.11. +- redis-backend (REASONED; docs): External Redis is opt-in GCS fault-tolerance storage; Redis credentials do not authenticate GCS clients and RAY_REDIS_PASSWORD is not a head flag substitute. +- token-default (REASONED; token-min, ray): Shared token authentication exists since 2.52.0 but is disabled by default as of 2.58.0; it supplements network isolation and does not protect Serve apps. +- token-input (REASONED; ray): Enable RAY_AUTH_MODE=token; precedence is RAY_AUTH_TOKEN, RAY_AUTH_TOKEN_PATH then ~/.ray/auth_token, shared by every node/client. +- token-storage (REASONED; ray): Generate/protect token files and copy before startup; tokens are plaintext, do not expire and must never be committed. +- token-transport (REASONED; docs, ray): Token HTTP headers require a trusted tunnel or TLS; plaintext HTTP exposes them on the network. +- kuberay-token (REASONED; kuberay-auth, kuberay-auth-source, kuberay-auth-prior): KubeRay v1.6.0 defines authOptions; v1.5.0 RayClusterSpec lacks it. With token mode enabled, the controller creates a random-token Secret when Kubernetes ServiceAccount token authentication is off, SecretName is null or empty, and the generated-name Secret does not already exist; it leaves an existing generated-name Secret or a supplied named Secret in place and creates no Secret in ServiceAccount mode. The pod builder sets RAY_AUTH_MODE when unset on head and worker Ray containers, on the wait-gcs-ready init container when present, and on the head autoscaler when autoscaling is enabled; outside ServiceAccount mode it adds RAY_AUTH_TOKEN from the selected Secret when unset, and in ServiceAccount mode it does not add RAY_AUTH_TOKEN. This guide infers support in v1.6.0 and later from the compared tags; later releases have not been checked. +- mfa (REASONED; docs): Ray has no user accounts; MFA belongs on SSH, tailnet or identity-aware proxy access. +- grpc-tls (REASONED; docs): RAY_USE_TLS defaults 0; export it and server certificate/key/CA variables on every node before startup for internal gRPC mutual TLS. +- tls-scope (REASONED; docs): gRPC TLS can cost performance and does not replace isolation or the dashboard/Jobs HTTP TLS proxy. +- runtime-code (REASONED; docs): Job runtime_env installs before entrypoint; ray.init environments apply to child tasks/actors. Packages, archives, py_executable and setup hooks execute submitted code. +- runtime-policy (REASONED; docs): No built-in package/URI/hook admission policy; eager_install and setup_timeout_seconds control timing, not trust. Prebuild reviewed dependencies. +- serve-bind (REASONED; docs): Serve HTTP defaults port 8000, Python host 127.0.0.1 but config-file host 0.0.0.0; EveryNode puts proxies on nodes with replicas. +- serve-auth (REASONED; docs): Serve apps have no built-in application auth and are outside cluster token mode; restrict origins and add proxy or FastAPI authentication. +- serve-tls (REASONED; docs): Serve ssl_keyfile/ssl_certfile/ssl_ca_certs default None; supplying a CA does not itself require client certificates. +- serve-grpc (REASONED; docs, ray): Optional Serve gRPC uses 9000 only with configured servicer functions. +- image-privilege (REASONED; ray): Official image ray UID 1000/GID 100 has passwordless sudo; non-root startup alone does not prevent root escalation. +- pod-security (REASONED; kuberay, kubernetes): KubeRay v1.7.0 head/worker pod/container contexts default empty; enforce non-root UID/GID, no privilege escalation, dropped capabilities and RuntimeDefault seccomp. +- verify-inventory (REASONED; ray): Inventory every head/worker and actual randomized agent ports after restarts; expected missing listeners need explanation. No isolated multi-node cluster was available. +- verify-external (REASONED; docs, curl): Probe every routed IPv4/IPv6 origin and inventoried port; completed TCP, even HTTP 401 or non-HTTP, proves exposure. Resolver/local errors are inconclusive. +- verify-jobs (REASONED; ray, kuberay-auth): Through the trusted tunnel valid-token submission must succeed and token-free submission must fail authentication with 401, not a connection error. +- verify-grpc-tls (REASONED; docs): ray health-check must pass with the correct CA and fail certificate verification with a wrong CA/name; local config errors are inconclusive. +- verify-agent (REASONED; ray): Direct non-browser /logs/ must expose an index with token mode off, allow a valid token with it on and reject absent tokens with 401; health exemptions cannot discriminate. +- verify-token-secret (REASONED; ray, curl): Trusted-path hidden prompt and stdin header avoid new token argv/history exposure and set +x avoids tracing; account owner/root can still read memory. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "286cb3508e87ad0658bd57c73270ab3b4100681250bae339f4d71a573e537bfb", + "components": { + "ray": { + "name": "Ray pinned source", + "basis": "ray-2.58.0", + "sources": { + "sed3f197cc1ef": "https://github.com/ray-project/ray/blob/ray-2.58.0/doc/source/ray-security/token-auth.md", + "s08d31327265a": "https://raw.githubusercontent.com/ray-project/ray/ray-2.58.0/docker/base-deps/Dockerfile", + "s2740a1880e2b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/rpc/grpc_server.cc#L67-L68", + "s958054ab801b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.h#L111-L113", + "sa3d2966f536b": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L601-L617", + "s050b349f5bbd": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L185", + "sd7d2d0d963d0": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.cc#L257-L278", + "s329ab4e0ed63": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/serve/_private/replica.py#L1778", + "s3eb2c2cb2cbb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/util/client/server/server.py#L799-L801", + "sae1bb5af6e35": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/util/client/server/proxier.py#L929-L931", + "s207ba00c4e86": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1572-L1575", + "s3efda42d7913": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L2464-L2471", + "s9a060fe08a53": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L189", + "sb1935b32e677": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L618-L635", + "se9af8ec90932": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L687-L692", + "s17bc66b5543e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L20-L23", + "s7df4fce455ca": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L54-L66", + "sc96f32b8a459": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L107-L113", + "sbca5ef4de0bb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/authentication/http_token_authentication.py#L28-L80", + "scdfff3ba79f6": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/optional_utils.py#L132-L209", + "s528944bdec23": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_agent.py#L32-L196", + "s4796e2ca38da": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_head.py#L123-L144", + "s1d86b2f31870": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_head.py#L267-L280", + "s9976ecf5b1f1": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L245-L249", + "sa542601368d1": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L264-L307", + "sd12bacfbc3c7": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/log/log_agent.py#L345-L414", + "s7ef1edbcda5c": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L570-L638", + "s6f5310eb313d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L144-L169", + "s8f508c7e2c94": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L182-L187", + "s48b3469099a3": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/agent.py#L100-L117", + "s532a98227d6d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L372-L377", + "s33a5ff7c987b": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1660-L1669", + "s1a19fd793b8f": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/node.py#L1768", + "s8f1ebe4bd911": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1381-L1387", + "s7682a13e83a2": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1862-L1883", + "s9237e74ea054": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1918-L1929", + "s4ce6fe077ba9": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L2022-L2031", + "s7f27125f73b7": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L504-L516", + "sf921a5d6a898": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/prometheus_exporter.py#L326-L334", + "s434c7b4364dc": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L218-L224", + "s08aa128e5d70": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L243-L248", + "sdd8960062a9c": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/includes/network_util.pxi#L103-L110", + "s51476b743c19": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/util/network_util.cc#L280-L289", + "s7451c8e00be3": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/scripts/scripts.py#L850-L854", + "s6c7faadf88a4": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L786-L821", + "s3625ec280c5e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/worker.py#L1835-L1836", + "sd3a18ab2a112": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/ray_constants.py#L507-L514", + "sba3f67581471": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/parameter.py#L165-L167", + "s994d0ddb6a7d": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/worker.py#L1884-L1912", + "sd4d44da36278": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/utils.py#L1128-L1142", + "s5a9d9c171f49": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/services.py#L1909-L1913", + "s9cd95d66d32a": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/utils.py#L322-L351", + "s762672f680e9": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/reporter/reporter_agent.py#L2099-L2101", + "s28e3a1076d01": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/runtime_env/agent/main.py#L23-L33", + "s883e3e00e4df": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/common/ray_config_def.h#L618-L619", + "sdb1c4564137b": "https://github.com/ray-project/ray/blob/ray-2.58.0/src/ray/raylet/node_manager.cc#L3527-L3530", + "sa7d4f677aadb": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_agent.py#L198-L203", + "sa5873642c8a2": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_manager.py#L421-L463", + "s2d562f0e7567": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/modules/job/job_manager.py#L565-L609", + "s3a079f7e3d82": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/_private/prometheus_exporter.py#L10", + "sefae1a66c0dd": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/dashboard/http_server_agent.py#L102-L114", + "scc7af6be456e": "https://github.com/ray-project/ray/blob/ray-2.58.0/python/ray/serve/config.py#L955-L963" + } + }, + "token-min": { + "name": "Ray token authentication introduction", + "basis": "2.52.0", + "sources": { + "sa5e2c2d58c12": "https://docs.ray.io/en/latest/ray-security/index.html" + } + }, + "docs": { + "name": "Ray documentation", + "basis": "unknown", + "sources": { + "sa5e2c2d58c12": "https://docs.ray.io/en/latest/ray-security/index.html", + "s6f0aa11f549a": "https://docs.ray.io/en/latest/ray-security/token-auth.html", + "s56c7c98ab722": "https://docs.ray.io/en/latest/cluster/cli.html", + "s261570b2672b": "https://docs.ray.io/en/latest/ray-core/configure.html", + "sc375ff2ca01f": "https://docs.ray.io/en/latest/ray-core/handling-dependencies.html", + "sb7a1d0b7dfb3": "https://docs.ray.io/en/latest/ray-core/api/doc/ray.runtime_env.RuntimeEnv.html", + "scd3f7d8a9b02": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.config.HTTPOptions.html", + "s15aa6ec80fb8": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.schema.HTTPOptionsSchema.html", + "s045d75fbdecc": "https://docs.ray.io/en/latest/serve/api/doc/ray.serve.schema.ServeDeploySchema.html", + "sc98a37c2b157": "https://docs.ray.io/en/latest/serve/http-guide.html", + "s54ed4b890b65": "https://docs.ray.io/en/latest/cluster/kubernetes/user-guides/kuberay-gcs-ft.html" + } + }, + "redis-history": { + "name": "Ray Redis history announcement", + "basis": "unknown", + "sources": { + "sb5afda59324d": "https://www.anyscale.com/blog/redis-in-ray-past-and-future" + } + }, + "kuberay-auth": { + "name": "KubeRay authentication documentation", + "basis": "unknown", + "sources": { + "saf5247262bc7": "https://docs.ray.io/en/latest/cluster/kubernetes/user-guides/kuberay-auth.html" + } + }, + "kuberay": { + "name": "KubeRay chart", + "basis": "v1.7.0", + "sources": { + "s76e45858aa01": "https://github.com/ray-project/kuberay/blob/v1.7.0/helm-chart/ray-cluster/values.yaml" + } + }, + "kuberay-auth-source": { + "name": "KubeRay authentication source", + "basis": "v1.6.0", + "sources": { + "s9a9f9edcc07e": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/apis/ray/v1/raycluster_types.go#L17-L19", + "s51c84c0bc3b4": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/apis/ray/v1/raycluster_types.go#L81-L107", + "s61e750cdc2e0": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/raycluster_controller.go#L369-L433", + "s6171707b3edb": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L194-L220", + "s408a3a202322": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L234-L236", + "s8505add03e5a": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L253-L271", + "s1cb4f972131a": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L296-L336", + "sd1b672bc4edd": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/common/pod.go#L460-L462", + "se37635162dc9": "https://github.com/ray-project/kuberay/blob/v1.6.0/ray-operator/controllers/ray/utils/util.go#L760-L766" + } + }, + "kuberay-auth-prior": { + "name": "KubeRay prior RayCluster types", + "basis": "v1.5.0", + "sources": { + "s04c5ce0b599d": "https://github.com/ray-project/kuberay/blob/v1.5.0/ray-operator/apis/ray/v1/raycluster_types.go#L12-L47" + } + }, + "docker": { + "name": "Docker publishing documentation", + "basis": "unknown", + "sources": { + "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" + } + }, + "kubernetes": { + "name": "Kubernetes security contexts", + "basis": "unknown", + "sources": { + "sb78ea91d0308": "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/" + } + }, + "docker-release": { + "name": "Docker Engine release notes", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "sd733f57ded23": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/release-notes/28.md#L768-L768" + } + }, + "python": { + "name": "CPython WSGI server", + "basis": "v3.11.0", + "sources": { + "sdd5a8e2ab59c": "https://github.com/python/cpython/blob/v3.11.0/Lib/wsgiref/simple_server.py#L1-L160", + "se4933ce3da65": "https://github.com/python/cpython/blob/v3.11.0/Lib/http/server.py#L120-L145", + "s0020052d7267": "https://github.com/python/cpython/blob/v3.11.0/Lib/socketserver.py#L400-L480" + } + }, + "curl": { + "name": "curl minimum write-out version", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + } + }, + "claims": { + "boundary": {"text": "Dashboard, Jobs and Client access permits arbitrary code execution; use an isolated cluster network and separate clusters for mutually untrusted jobs.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12"], "status": "REASONED"}, + "dashboard": {"text": "Dashboard defaults to resolved localhost, IPv4 before IPv6 then 127.0.0.1, on port 8265; state loopback explicitly.", "components": ["ray"], "sources": ["ray:sa3d2966f536b", "ray:s050b349f5bbd", "ray:sd7d2d0d963d0"], "status": "REASONED"}, + "docker": {"text": "Bridge publishing uses host loopback and container-interface bind; this guide assesses Docker 28.0.0 or later with normal NAT as the loopback-isolation minimum because the 28.0.0 release notes record a fix for neighbour hosts reaching loopback-mapped ports.", "components": ["docker", "docker-release"], "sources": ["docker:s1e53417c513d", "docker-release:sd733f57ded23"], "status": "REASONED"}, + "access": {"text": "Reach Dashboard/Jobs through loopback SSH forwarding, launcher forwarding, KubeRay forwarding or a private tailnet; public browser access needs an authenticating TLS proxy.", "components": ["docs", "kuberay-auth"], "sources": ["docs:sa5e2c2d58c12", "docs:s56c7c98ab722", "kuberay-auth:saf5247262bc7"], "status": "REASONED"}, + "head-ports": {"text": "Head GCS uses 6379 and Client 10001; worker ports default 10002-19999 plus randomized listeners.", "components": ["docs"], "sources": ["docs:s56c7c98ab722", "docs:s261570b2672b"], "status": "REASONED"}, + "core-bind": {"text": "Core gRPC source binds all interfaces unless node address is exactly 127.0.0.1, ::1 or localhost; private node IP does not imply private bind.", "components": ["ray"], "sources": ["ray:s2740a1880e2b", "ray:s958054ab801b"], "status": "REASONED"}, + "gcs-observation": {"text": "Four attempted Ray 2.58.0 loopback starts still showed GCS *:6379; the cause was not established and the watcher stopped the runs.", "components": ["ray"], "sources": ["ray:s2740a1880e2b", "ray:s958054ab801b"], "status": "DEMONSTRATED", "evidence": "in each of four runs using `ray start --head --node-ip-address=127.0.0.1 --port=6379 --dashboard-host=127.0.0.1`, `ss` still showed the GCS listener as `*:6379`."}, + "client-bind": {"text": "Ray Client server/proxier listen on the supplied node address plus loopback; restrict access and prefer Jobs over publishing Client.", "components": ["ray"], "sources": ["ray:s3eb2c2cb2cbb", "ray:sae1bb5af6e35", "ray:s207ba00c4e86", "ray:s3efda42d7913"], "status": "REASONED"}, + "serve-replica": {"text": "Serve inter-deployment gRPC binds [::] on an OS-chosen port; this is source reasoning, not a run.", "components": ["ray"], "sources": ["ray:s329ab4e0ed63"], "status": "REASONED"}, + "agents": {"text": "--include-dashboard=false does not stop dashboard/runtime-env agents; head dashboard itself is reduced to usage stats.", "components": ["ray"], "sources": ["ray:s33a5ff7c987b", "ray:s1a19fd793b8f", "ray:s8f1ebe4bd911", "ray:s7682a13e83a2", "ray:s9237e74ea054", "ray:s4ce6fe077ba9"], "status": "REASONED"}, + "minimal": {"text": "Missing optional imports select minimal mode: no agent HTTP/gRPC or metrics reporter; runtime-env HTTP remains. Base installs may have full dependencies.", "components": ["ray"], "sources": ["ray:s48b3469099a3", "ray:sd4d44da36278", "ray:s5a9d9c171f49", "ray:s9cd95d66d32a", "ray:s762672f680e9", "ray:s28e3a1076d01"], "status": "REASONED"}, + "agent-http": {"text": "Non-minimal agent HTTP defaults 52365, including ray.init; --dashboard-agent-listen-port changes it, with node-IP plus conditional localhost binds.", "components": ["ray"], "sources": ["ray:s9a060fe08a53", "ray:sb1935b32e677", "ray:s7df4fce455ca", "ray:sba3f67581471", "ray:s994d0ddb6a7d"], "status": "REASONED"}, + "agent-jobs": {"text": "Direct /api/job_agent/jobs/ submission and stop/delete/log routes bypass a proxy on 8265; head forwarding reaches its agent job manager.", "components": ["ray"], "sources": ["ray:s528944bdec23", "ray:s4796e2ca38da", "ray:s1d86b2f31870"], "status": "REASONED"}, + "agent-placement": {"text": "Worker agents accept cluster submissions with their own managers; drivers normally use the head unless resources, labels or worker-placement override permit workers.", "components": ["ray"], "sources": ["ray:sa7d4f677aadb", "ray:sa5873642c8a2", "ray:s2d562f0e7567"], "status": "REASONED"}, + "agent-logs": {"text": "Agent static /logs exposes the node log directory and index.", "components": ["ray"], "sources": ["ray:s9976ecf5b1f1"], "status": "REASONED"}, + "agent-http-auth": {"text": "Without token mode a browser heuristic is the only filter; token mode guards all agent HTTP paths except two health endpoints.", "components": ["ray"], "sources": ["ray:s17bc66b5543e", "ray:sc96f32b8a459", "ray:sbca5ef4de0bb", "ray:scdfff3ba79f6"], "status": "REASONED"}, + "agent-grpc": {"text": "Agent gRPC defaults OS-assigned with --dashboard-agent-grpc-port override; localhost or wildcard bind serves log/profiling RPCs with optional token interception.", "components": ["ray"], "sources": ["ray:sb1935b32e677", "ray:sa542601368d1", "ray:sd12bacfbc3c7", "ray:s7ef1edbcda5c", "ray:s6f5310eb313d", "ray:s8f508c7e2c94", "ray:s532a98227d6d"], "status": "REASONED"}, + "runtime-agent": {"text": "Runtime-env HTTP defaults OS-assigned on node IP with --runtime-env-agent-port override; create/delete installs packages, and token middleware exempts no route.", "components": ["ray"], "sources": ["ray:s532a98227d6d", "ray:s434c7b4364dc", "ray:s08aa128e5d70", "ray:sb1935b32e677"], "status": "REASONED"}, + "metrics-agent": {"text": "Metrics default enabled; non-minimal reporter uses an OS-assigned port or --metrics-export-port, outside all token middleware.", "components": ["ray"], "sources": ["ray:s532a98227d6d", "ray:s7f27125f73b7", "ray:sf921a5d6a898", "ray:s883e3e00e4df", "ray:sdb1c4564137b", "ray:se9af8ec90932"], "status": "REASONED"}, + "metrics-ipv4": {"text": "WSGI metrics uses AF_INET: empty address is IPv4 wildcard, localhost is used for local node addresses, and resolved ::1 cannot bind this socket.", "components": ["ray", "python"], "sources": ["ray:s7f27125f73b7", "ray:sf921a5d6a898", "ray:s3a079f7e3d82", "python:sdd5a8e2ab59c", "python:se4933ce3da65", "python:s0020052d7267"], "status": "REASONED"}, + "node-normalization": {"text": "Both CLI and ray.init normalize localhost node arguments to a discovered address; cluster-mode setting changes whole-node selection, not independent agent binds.", "components": ["ray"], "sources": ["ray:s7451c8e00be3", "ray:s6c7faadf88a4", "ray:s3625ec280c5e", "ray:sd3a18ab2a112"], "status": "REASONED"}, + "agent-family": {"text": "Agent gRPC wildcard selection is 0.0.0.0 or :: based on resolved localhost, independently of node address family.", "components": ["ray"], "sources": ["ray:sdd8960062a9c", "ray:s51476b743c19"], "status": "REASONED"}, + "gcs-not-redis": {"text": "6379 is GCS, not Redis; Redis stopped being launched by default in 1.11.", "components": ["redis-history", "docs"], "sources": ["redis-history:sb5afda59324d", "docs:s56c7c98ab722"], "status": "REASONED"}, + "redis-backend": {"text": "External Redis is opt-in GCS fault-tolerance storage; Redis credentials do not authenticate GCS clients and RAY_REDIS_PASSWORD is not a head flag substitute.", "components": ["docs"], "sources": ["docs:s56c7c98ab722", "docs:s54ed4b890b65"], "status": "REASONED"}, + "token-default": {"text": "Shared token authentication exists since 2.52.0 but is disabled by default as of 2.58.0; it supplements network isolation and does not protect Serve apps.", "components": ["token-min", "ray"], "sources": ["token-min:sa5e2c2d58c12", "ray:sed3f197cc1ef"], "status": "REASONED"}, + "token-input": {"text": "Enable RAY_AUTH_MODE=token; precedence is RAY_AUTH_TOKEN, RAY_AUTH_TOKEN_PATH then ~/.ray/auth_token, shared by every node/client.", "components": ["ray"], "sources": ["ray:sed3f197cc1ef"], "status": "REASONED"}, + "token-storage": {"text": "Generate/protect token files and copy before startup; tokens are plaintext, do not expire and must never be committed.", "components": ["ray"], "sources": ["ray:sed3f197cc1ef"], "status": "REASONED"}, + "token-transport": {"text": "Token HTTP headers require a trusted tunnel or TLS; plaintext HTTP exposes them on the network.", "components": ["docs", "ray"], "sources": ["docs:s6f0aa11f549a", "ray:sed3f197cc1ef"], "status": "REASONED"}, + "kuberay-token": {"text": "KubeRay v1.6.0 defines authOptions; v1.5.0 RayClusterSpec lacks it. With token mode enabled, the controller creates a random-token Secret when Kubernetes ServiceAccount token authentication is off, SecretName is null or empty, and the generated-name Secret does not already exist; it leaves an existing generated-name Secret or a supplied named Secret in place and creates no Secret in ServiceAccount mode. The pod builder sets RAY_AUTH_MODE when unset on head and worker Ray containers, on the wait-gcs-ready init container when present, and on the head autoscaler when autoscaling is enabled; outside ServiceAccount mode it adds RAY_AUTH_TOKEN from the selected Secret when unset, and in ServiceAccount mode it does not add RAY_AUTH_TOKEN. This guide infers support in v1.6.0 and later from the compared tags; later releases have not been checked.", "components": ["kuberay-auth", "kuberay-auth-source", "kuberay-auth-prior"], "sources": ["kuberay-auth:saf5247262bc7", "kuberay-auth-source:s9a9f9edcc07e", "kuberay-auth-source:s51c84c0bc3b4", "kuberay-auth-source:s61e750cdc2e0", "kuberay-auth-source:s6171707b3edb", "kuberay-auth-source:s408a3a202322", "kuberay-auth-source:s8505add03e5a", "kuberay-auth-source:s1cb4f972131a", "kuberay-auth-source:sd1b672bc4edd", "kuberay-auth-source:se37635162dc9", "kuberay-auth-prior:s04c5ce0b599d"], "status": "REASONED"}, + "mfa": {"text": "Ray has no user accounts; MFA belongs on SSH, tailnet or identity-aware proxy access.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12"], "status": "REASONED"}, + "grpc-tls": {"text": "RAY_USE_TLS defaults 0; export it and server certificate/key/CA variables on every node before startup for internal gRPC mutual TLS.", "components": ["docs"], "sources": ["docs:s261570b2672b"], "status": "REASONED"}, + "tls-scope": {"text": "gRPC TLS can cost performance and does not replace isolation or the dashboard/Jobs HTTP TLS proxy.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12", "docs:s261570b2672b"], "status": "REASONED"}, + "runtime-code": {"text": "Job runtime_env installs before entrypoint; ray.init environments apply to child tasks/actors. Packages, archives, py_executable and setup hooks execute submitted code.", "components": ["docs"], "sources": ["docs:sc375ff2ca01f"], "status": "REASONED"}, + "runtime-policy": {"text": "No built-in package/URI/hook admission policy; eager_install and setup_timeout_seconds control timing, not trust. Prebuild reviewed dependencies.", "components": ["docs"], "sources": ["docs:sc375ff2ca01f", "docs:sb7a1d0b7dfb3"], "status": "REASONED"}, + "serve-bind": {"text": "Serve HTTP defaults port 8000, Python host 127.0.0.1 but config-file host 0.0.0.0; EveryNode puts proxies on nodes with replicas.", "components": ["docs"], "sources": ["docs:scd3f7d8a9b02", "docs:s15aa6ec80fb8", "docs:s045d75fbdecc"], "status": "REASONED"}, + "serve-auth": {"text": "Serve apps have no built-in application auth and are outside cluster token mode; restrict origins and add proxy or FastAPI authentication.", "components": ["docs"], "sources": ["docs:sa5e2c2d58c12", "docs:sc98a37c2b157"], "status": "REASONED"}, + "serve-tls": {"text": "Serve ssl_keyfile/ssl_certfile/ssl_ca_certs default None; supplying a CA does not itself require client certificates.", "components": ["docs"], "sources": ["docs:scd3f7d8a9b02"], "status": "REASONED"}, + "serve-grpc": {"text": "Optional Serve gRPC uses 9000 only with configured servicer functions.", "components": ["docs", "ray"], "sources": ["docs:s045d75fbdecc", "ray:scc7af6be456e"], "status": "REASONED"}, + "image-privilege": {"text": "Official image ray UID 1000/GID 100 has passwordless sudo; non-root startup alone does not prevent root escalation.", "components": ["ray"], "sources": ["ray:s08d31327265a"], "status": "REASONED"}, + "pod-security": {"text": "KubeRay v1.7.0 head/worker pod/container contexts default empty; enforce non-root UID/GID, no privilege escalation, dropped capabilities and RuntimeDefault seccomp.", "components": ["kuberay", "kubernetes"], "sources": ["kuberay:s76e45858aa01", "kubernetes:sb78ea91d0308"], "status": "REASONED"}, + "verify-inventory": {"text": "Inventory every head/worker and actual randomized agent ports after restarts; expected missing listeners need explanation. No isolated multi-node cluster was available.", "components": ["ray"], "sources": ["ray:s9a060fe08a53", "ray:s6f5310eb313d", "ray:s532a98227d6d", "ray:s7f27125f73b7", "ray:s434c7b4364dc"], "status": "REASONED", "verify": [1]}, + "verify-external": {"text": "Probe every routed IPv4/IPv6 origin and inventoried port; completed TCP, even HTTP 401 or non-HTTP, proves exposure. Resolver/local errors are inconclusive.", "components": ["docs", "curl"], "sources": ["docs:sa5e2c2d58c12", "docs:s261570b2672b", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, + "verify-jobs": {"text": "Through the trusted tunnel valid-token submission must succeed and token-free submission must fail authentication with 401, not a connection error.", "components": ["ray", "kuberay-auth"], "sources": ["ray:sed3f197cc1ef", "kuberay-auth:saf5247262bc7"], "status": "REASONED", "verify": [1]}, + "verify-grpc-tls": {"text": "ray health-check must pass with the correct CA and fail certificate verification with a wrong CA/name; local config errors are inconclusive.", "components": ["docs"], "sources": ["docs:s261570b2672b"], "status": "REASONED", "verify": [1]}, + "verify-agent": {"text": "Direct non-browser /logs/ must expose an index with token mode off, allow a valid token with it on and reject absent tokens with 401; health exemptions cannot discriminate.", "components": ["ray"], "sources": ["ray:s9976ecf5b1f1", "ray:s17bc66b5543e", "ray:sbca5ef4de0bb", "ray:sefae1a66c0dd"], "status": "REASONED", "verify": [2]}, + "verify-token-secret": {"text": "Trusted-path hidden prompt and stdin header avoid new token argv/history exposure and set +x avoids tracing; account owner/root can still read memory.", "components": ["ray", "curl"], "sources": ["ray:sed3f197cc1ef", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]} + } +} +```` + + ====================================================================== ==> mlflow.md ====================================================================== @@ -41064,108 +43668,9 @@ Claims (full source mapping in the JSON block below): # Workflow and agent orchestrators: Prefect, Dagster, Airflow, Temporal, Flower, Argo - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-05; documentation checked 2026-10 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- prefect: Prefect Basic Auth minimum (3.1.8) -- prefect-source: Prefect server source (9e560c9b6df4e19a5109a66e66d461f9facb538d) -- dagster: Dagster OSS (1.13.24) -- airflow: Apache Airflow (3.3.2) -- airflow-two: Apache Airflow historical configuration (2.11.0) -- airflow-old: Apache Airflow historical default (2.2.5) -- flower: Flower (v2.2.0) -- tornado: Tornado (v6.5.10) -- linux: Linux permissions and tools (unknown) -- systemd: systemd (v257) -- fhs: Filesystem Hierarchy Standard (3.0) -- docker: Moby Docker (docker-v29.8.1) -- argo-three: Argo Workflows (v3.7.18) -- argo-four: Argo Workflows (v4.1.4) -- argo-docs: Argo Workflows documentation (release-3.7) -- iproute2: iproute2 ss manual (v6.15.0) -- network-namespaces: Linux network namespaces manual (man-pages-5.13) -- prefect-rolling: Prefect documentation (rolling) (unknown) -- dagster-rolling: Dagster documentation (rolling) (unknown) -- airflow-rolling: Apache Airflow documentation (rolling) (unknown) -- temporal-rolling: Temporal documentation (rolling) (unknown) -- flower-rolling: Flower documentation (rolling) (unknown) -- prefect-blocks: Prefect Blocks source (3.1.8) -- fab-source: Apache Airflow FAB provider source (providers-fab/3.9.0) -- airflow-crypto: Apache Airflow crypto source (3.3.2) -- temporal-codec: Temporal Go SDK codec source (v1.28.0) -- fab-rolling: Apache Airflow FAB provider documentation (rolling) (unknown) - -Claims (full source mapping in the front matter): +Written against Prefect Basic Auth minimum 3.1.8, Prefect Blocks source 3.1.8, Dagster OSS 1.13.24, Apache Airflow 3.3.2, Apache Airflow historical configuration 2.11.0, Apache Airflow historical default 2.2.5, and Apache Airflow FAB provider source providers-fab/3.9.0, plus 11 more recorded bases; 1 further source is pinned to a commit and 7 carry no recorded version. Metadata reviewed 2026-10-05; documentation checked October 2026. The guide records 62 claims: 3 demonstrated by a check that ran and 59 reasoned from cited sources. -- fronting (REASONED; airflow, argo-docs, temporal-rolling): Keep UI/API origins private behind HTTPS and authentication; use IdP MFA for browsers and separate machine authentication, and constrain worker egress. -- prefect-auth (REASONED; prefect, prefect-rolling): Self-hosted Prefect defaults unauthenticated; Basic Auth from 3.1.8 uses matching server/client auth strings and prompts the UI. -- prefect-key (REASONED; prefect, prefect-rolling): Cloud-only PREFECT_API_KEY takes precedence over PREFECT_API_AUTH_STRING and produces 401 against a self-hosted server. -- prefect-secrets (REASONED; prefect-blocks, prefect-rolling): Protect the auth string and credential-bearing Blocks; Prefect 3.1.8 block-document read routes accept include_secrets, and Secret.get() returns the underlying value. Restrict API and worker access; UI masking does not enforce authorization. -- prefect-port (REASONED; prefect-rolling): The self-hosted Prefect server uses port 4200. -- dagster-bind (REASONED; dagster): Direct dagster-webserver/dev defaults to 127.0.0.1:3000, with flag/environment overrides and free-port fallback when the port is unset or 0; explicitly bind the private origin. -- dagster-helm (REASONED; dagster): The chart passes -h 0.0.0.0 on the Service port, default 80, with ClusterIP Service by default. -- dagster-auth (REASONED; dagster-rolling): OSS webserver has no built-in login or access control; protect all routes with an authenticating proxy. -- airflow-boundary (REASONED; airflow): Airflow assumes authenticated known users and is not designed for untrusted public exposure; deployment managers must keep it private. -- airflow-two-api (REASONED; airflow-two, airflow-old): Airflow 2.11.0 uses [api] auth_backends with session default; before 2.3 the name was auth_backend and 2.2.5 defaulted to deny_all. -- airflow-simple (REASONED; airflow): Airflow 3 defaults to development-only Simple Auth Manager, with configured users/roles and generated passwords printed to logs unless supplied. -- airflow-fab (REASONED; airflow, fab-rolling): For production install FAB and select FabAuthManager through [core] auth_manager; verify the effective manager and use an identity backend with MFA. -- airflow-api (REASONED; airflow, fab-rolling): Airflow 3 public API uses JWT independently of [fab] auth_backends, which selects FAB API backends rather than the auth manager. -- airflow-all-admins (REASONED; airflow): Keep simple_auth_manager_all_admins unset or False; enabling it disables login and grants every visitor admin. -- fab-public-role (REASONED; fab-source): Leave [fab] auth_role_public and legacy AUTH_ROLE_PUBLIC unset; a configured public role can make FAB treat an anonymous visitor as logged in and supply that role's permissions. -- airflow-compose-account (REASONED; airflow-rolling, fab-rolling, airflow): Development Compose selects FAB but defaults to airflow/airflow; set credentials before account creation and update/delete an existing account separately. -- airflow-jwt (REASONED; airflow-rolling, airflow): Replace Compose fallback AIRFLOW__API_AUTH__JWT_SECRET=airflow_jwt_secret and share the strong value with all signers/validators; the public fallback permits token forgery. -- airflow-secret-key (REASONED; airflow-two, airflow-rolling, airflow): Provision the independent secret_key too: [webserver] on 2.11.0 and [api] on 3.3.2. -- airflow-config (REASONED; airflow-two, airflow-rolling, airflow): Keep configuration exposure off with WEBSERVER__EXPOSE_CONFIG on 2.11.0 or API__EXPOSE_CONFIG on 3.3.2. -- airflow-fernet (REASONED; airflow-crypto, airflow-rolling): Protect Connections, Variables, database and Fernet keys; an empty [core] FERNET_KEY selects a no-encryption implementation for new values, while configured comma-separated keys use MultiFernet. Preserve keys needed to decrypt existing values; authorized workloads still use secrets. -- airflow-port (REASONED; airflow-rolling): Compose publishes 8080 on all interfaces; use 127.0.0.1:8080:8080 behind the fronting layer. -- temporal-authorizer (REASONED; temporal-rolling): Empty authorizer selects noopAuthorizer allowing every API request, including administration; configure default authorization with trusted JWT keys and audience. -- temporal-claims (REASONED; temporal-rolling): Empty claimMapper selects a no-op granting system-admin claims; configure both Authorizer and ClaimMapper, with frontend TLS. -- temporal-health (REASONED; temporal-rolling): The built-in default authorizer permits health checks without claims; health is not an authorization discriminator. -- temporal-ui (REASONED; temporal-rolling): UI OIDC auth.enabled is a sibling of providers; TEMPORAL_AUTH_ENABLED plus provider settings gates the UI only, with MFA at the IdP. -- temporal-tls (REASONED; temporal-rolling): Internode and frontend mTLS are separate from UI login and API authorization. -- temporal-payloads (REASONED; temporal-rolling, temporal-codec): Keep plaintext credentials out of persisted inputs/results/history; the Temporal Go SDK v1.28.0 codec HTTP handler accepts encode/decode POSTs without an authentication check, so authenticate and restrict a deployed Codec Server separately. -- temporal-ports (REASONED; temporal-rolling): Frontend gRPC uses 7233 and the Web UI 8233 in the cited CLI server reference. -- flower-bind (REASONED; flower, tornado): Flower defaults to wildcard address and 5555 unless Unix socket, FLOWER_ variables or implicit working-directory flowerconfig.py override; bind loopback explicitly. -- flower-api (REASONED; flower-rolling): Authentication defaults off; unauthenticated HTTP API is disabled unless FLOWER_UNAUTHENTICATED_API=true, which must not be mistaken for dashboard protection. -- flower-basic (REASONED; flower-rolling): Basic auth uses a comma-separated credential list; supply it through FLOWER_BASIC_AUTH or protected config rather than argv and retain private TLS fronting. -- flower-oauth (REASONED; flower-rolling): OAuth needs handler, client key/secret, redirect and allowed-email regex; protect the secret outside argv and prefer an MFA-enforcing provider. -- flower-socket (REASONED; flower, tornado, linux): Flower passes fixed mode 0777; Tornado removes/recreates filesystem sockets on startup, so chmod does not persist. Abstract sockets have no filesystem permission boundary. -- flower-directory (REASONED; linux): Restrict socket directory search with Flower ownership, proxy-only group, mode 0750 and no outsider ACL; directory access is essential because socket mode permits writes. -- flower-capabilities (REASONED; linux, docker): Applicable DAC capabilities bypass directory search checks; Docker defaults include CAP_DAC_OVERRIDE, while user namespaces and idmapped mounts change identity interpretation. -- flower-runtime-dir (REASONED; systemd, fhs): Reapply directory owner/group/mode on creation: /run is cleared at boot and RuntimeDirectory defaults 0755 unless RuntimeDirectoryMode overrides it. -- flower-numeric-ids (REASONED; linux): A proxy sharing the socket directory must hold the required numeric group after namespace/mount mappings; names alone do not establish access. -- argo-bind (REASONED; argo-three, argo-four): Argo Server binds wildcard :2746 with host-dependent IPv4/IPv6 support and no bind-address flag. -- argo-defaults (REASONED; argo-three, argo-four): Both tags default secure=true, auth-mode=client and hsts=true; flags and ARGO_ environment equivalents can override. -- argo-client (REASONED; argo-three, argo-four): Client mode uses caller Kubernetes credentials/RBAC; v4.1.4 validates via SelfSubjectReview and requires Kubernetes 1.28+. -- argo-quickstart (REASONED; argo-three, argo-four): Both tags' minimal/mysql/postgres quick-starts enable server plus client mode while retaining TLS; v4.1.4 telemetry does too. Anonymous callers inherit server permissions. -- argo-quickstart-rbac (REASONED; argo-three, argo-four): Quick-start binds cluster-wide workflow/template and Secret get/create permissions, plus cluster-workflow-template rights; it is demo-only. -- argo-installs (REASONED; argo-three, argo-four): Traced install inputs keep client auth/TLS with cluster-wide server RBAC; namespace-install adds --namespaced and namespace RBAC. Generated install YAML was absent. -- argo-service (REASONED; argo-three, argo-four): Base Service omits type, defaulting to ClusterIP, and maps 2746 to 2746; this does not authenticate reachable callers. -- argo-fallback (REASONED; argo-three, argo-four): Remove server auth mode: adding client/SSO does not prevent anonymous fallback to server credentials; local mode uses the server kubeconfig. -- argo-sso (REASONED; argo-docs, argo-three, argo-four): Enable sso.rbac.enabled and map groups to narrow ServiceAccounts through rbac-rule/precedence; without SSO RBAC, authenticated users share server permissions. -- argo-secrets (REASONED; argo-docs): Store SSO OAuth credentials in referenced Kubernetes Secrets and supply API tokens through protected input, not argv. -- argo-tls (REASONED; argo-three, argo-four): Keep TLS enabled with a trusted certificate Secret; no name generates a self-signed certificate, failed named-Secret loading fails startup, secure=false is plaintext and defeats HSTS. -- argo-metrics (REASONED; argo-three, argo-four): /metrics shares the server listener and auth gate unless ARGO_SERVER_METRICS_AUTH=false; server mode also admits anonymous metrics. -- argo-network (REASONED; argo-docs): Keep 2746 private behind authenticated HTTPS; restrict direct Pod/Service access and scope server, SSO and execution-account permissions. Workflow submission permits arbitrary containers unless constrained. -- verify-inventory (REASONED; flower, argo-three, iproute2, network-namespaces, prefect-rolling): ss inventories only the current namespace, not publication, routing or authentication; inspect publications and external reachability separately. -- verify-prefect (REASONED; prefect, prefect-source, prefect-rolling): Anonymous POST /api/flows/filter returning a JSON list is exposed; fixed is 401 with an authorized list on the same origin. Health/ready GET exemptions are version-dependent. -- verify-dagster (REASONED; dagster-rolling): A RepositoryConnection from /graphql is a read, including empty nodes; GraphQL/transport errors are inconclusive. Test proxy authentication and origin isolation separately. -- verify-airflow (REASONED; airflow-two, airflow-rolling, fab-rolling): FAB 3.9.0 returning 201 with access_token for airflow/airflow is exposed; pair a valid account and anonymous/authenticated API reads at proxy and origin. Airflow 2 uses its own API/auth. -- verify-temporal (REASONED; temporal-rolling): Credential-free workflow listing on 7233 is exposed; require rejection and a matched authorized call, not health, TLS errors or missing namespaces as proof of auth. -- verify-flower (REASONED; flower-rolling): Anonymous UI/API should yield Basic 401 or OAuth login redirect, with a valid-session control; API-disabled is not proof the dashboard is protected. -- verify-socket (REASONED; flower, linux): Directory/socket permissions, outsider denial, reboot persistence and container numeric IDs remain reasoned; no socket or Flower process ran. -- flower-stat-run (DEMONSTRATED; linux): Tmpfs directory-only stat distinguished 0755 and 0750; the 0750 directory initially had only base ACL entries. -- flower-acl-run (DEMONSTRATED; linux): A named nobody search ACL remained invisible in stat mode 750; ls and getfacl exposed the entry. -- flower-mask-run (DEMONSTRATED; linux): Changing the ACL mask hid named search permission; chmod 0750 restored its reported effectiveness. Access as nobody was not tested. -- verify-argo (REASONED; argo-three, argo-four, argo-docs): Anonymous workflow JSON is exposed when server RBAC permits; missing tokens should yield 401 without server mode. A 403 alone is inconclusive; pair authorized 200 and inspect auth modes. -- verify-origin (REASONED; airflow, argo-docs): Any HTTP response from the origin at an untrusted vantage proves publication; use the same responding allowed origin as control and treat connection errors/timeouts as inconclusive. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. These webservers and UIs schedule and trigger arbitrary code execution across your infrastructure, and most ship with no authentication at all. Keep every one of them off the public internet and add auth before @@ -41640,6 +44145,475 @@ These defaults are checked against Prefect 3.1.8+ for Basic Auth, Dagster 1.13.x - Apache Airflow webserver secret_key and EXPOSE_CONFIG settings (2.11.0 documentation): https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#secret-key, https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#expose-config - Apache Airflow API secret_key and EXPOSE_CONFIG settings (3.3.2 documentation): https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#secret-key, https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#expose-config + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/workflow-orchestrators.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- prefect: Prefect Basic Auth minimum (3.1.8) +- prefect-blocks: Prefect Blocks source (3.1.8) +- dagster: Dagster OSS (1.13.24) +- airflow: Apache Airflow (3.3.2) +- airflow-two: Apache Airflow historical configuration (2.11.0) +- airflow-old: Apache Airflow historical default (2.2.5) +- fab-source: Apache Airflow FAB provider source (providers-fab/3.9.0) +- airflow-crypto: Apache Airflow crypto source (3.3.2) +- temporal-codec: Temporal Go SDK codec source (v1.28.0) +- flower: Flower (v2.2.0) +- argo-three: Argo Workflows (v3.7.18) +- argo-four: Argo Workflows (v4.1.4) +- argo-docs: Argo Workflows documentation (release-3.7) +- prefect-source: Prefect server source (9e560c9b6df4e19a5109a66e66d461f9facb538d) +- prefect-rolling: Prefect documentation (rolling) (unknown) +- dagster-rolling: Dagster documentation (rolling) (unknown) +- airflow-rolling: Apache Airflow documentation (rolling) (unknown) +- fab-rolling: Apache Airflow FAB provider documentation (rolling) (unknown) +- temporal-rolling: Temporal documentation (rolling) (unknown) +- flower-rolling: Flower documentation (rolling) (unknown) +- tornado: Tornado (v6.5.10) +- docker: Moby Docker (docker-v29.8.1) +- linux: Linux permissions and tools (unknown) +- systemd: systemd (v257) +- fhs: Filesystem Hierarchy Standard (3.0) +- iproute2: iproute2 ss manual (v6.15.0) +- network-namespaces: Linux network namespaces manual (man-pages-5.13) + +Claims (full source mapping in the JSON block below): + +- fronting (REASONED; airflow, argo-docs, temporal-rolling): Keep UI/API origins private behind HTTPS and authentication; use IdP MFA for browsers and separate machine authentication, and constrain worker egress. +- prefect-auth (REASONED; prefect, prefect-rolling): Self-hosted Prefect defaults unauthenticated; Basic Auth from 3.1.8 uses matching server/client auth strings and prompts the UI. +- prefect-key (REASONED; prefect, prefect-rolling): Cloud-only PREFECT_API_KEY takes precedence over PREFECT_API_AUTH_STRING and produces 401 against a self-hosted server. +- prefect-secrets (REASONED; prefect-blocks, prefect-rolling): Protect the auth string and credential-bearing Blocks; Prefect 3.1.8 block-document read routes accept include_secrets, and Secret.get() returns the underlying value. Restrict API and worker access; UI masking does not enforce authorization. +- prefect-port (REASONED; prefect-rolling): The self-hosted Prefect server uses port 4200. +- dagster-bind (REASONED; dagster): Direct dagster-webserver/dev defaults to 127.0.0.1:3000, with flag/environment overrides and free-port fallback when the port is unset or 0; explicitly bind the private origin. +- dagster-helm (REASONED; dagster): The chart passes -h 0.0.0.0 on the Service port, default 80, with ClusterIP Service by default. +- dagster-auth (REASONED; dagster-rolling): OSS webserver has no built-in login or access control; protect all routes with an authenticating proxy. +- airflow-boundary (REASONED; airflow): Airflow assumes authenticated known users and is not designed for untrusted public exposure; deployment managers must keep it private. +- airflow-two-api (REASONED; airflow-two, airflow-old): Airflow 2.11.0 uses [api] auth_backends with session default; before 2.3 the name was auth_backend and 2.2.5 defaulted to deny_all. +- airflow-simple (REASONED; airflow): Airflow 3 defaults to development-only Simple Auth Manager, with configured users/roles and generated passwords printed to logs unless supplied. +- airflow-fab (REASONED; airflow, fab-rolling): For production install FAB and select FabAuthManager through [core] auth_manager; verify the effective manager and use an identity backend with MFA. +- airflow-api (REASONED; airflow, fab-rolling): Airflow 3 public API uses JWT independently of [fab] auth_backends, which selects FAB API backends rather than the auth manager. +- airflow-all-admins (REASONED; airflow): Keep simple_auth_manager_all_admins unset or False; enabling it disables login and grants every visitor admin. +- fab-public-role (REASONED; fab-source): Leave [fab] auth_role_public and legacy AUTH_ROLE_PUBLIC unset; a configured public role can make FAB treat an anonymous visitor as logged in and supply that role's permissions. +- airflow-compose-account (REASONED; airflow-rolling, fab-rolling, airflow): Development Compose selects FAB but defaults to airflow/airflow; set credentials before account creation and update/delete an existing account separately. +- airflow-jwt (REASONED; airflow-rolling, airflow): Replace Compose fallback AIRFLOW__API_AUTH__JWT_SECRET=airflow_jwt_secret and share the strong value with all signers/validators; the public fallback permits token forgery. +- airflow-secret-key (REASONED; airflow-two, airflow-rolling, airflow): Provision the independent secret_key too: [webserver] on 2.11.0 and [api] on 3.3.2. +- airflow-config (REASONED; airflow-two, airflow-rolling, airflow): Keep configuration exposure off with WEBSERVER__EXPOSE_CONFIG on 2.11.0 or API__EXPOSE_CONFIG on 3.3.2. +- airflow-fernet (REASONED; airflow-crypto, airflow-rolling): Protect Connections, Variables, database and Fernet keys; an empty [core] FERNET_KEY selects a no-encryption implementation for new values, while configured comma-separated keys use MultiFernet. Preserve keys needed to decrypt existing values; authorized workloads still use secrets. +- airflow-port (REASONED; airflow-rolling): Compose publishes 8080 on all interfaces; use 127.0.0.1:8080:8080 behind the fronting layer. +- temporal-authorizer (REASONED; temporal-rolling): Empty authorizer selects noopAuthorizer allowing every API request, including administration; configure default authorization with trusted JWT keys and audience. +- temporal-claims (REASONED; temporal-rolling): Empty claimMapper selects a no-op granting system-admin claims; configure both Authorizer and ClaimMapper, with frontend TLS. +- temporal-health (REASONED; temporal-rolling): The built-in default authorizer permits health checks without claims; health is not an authorization discriminator. +- temporal-ui (REASONED; temporal-rolling): UI OIDC auth.enabled is a sibling of providers; TEMPORAL_AUTH_ENABLED plus provider settings gates the UI only, with MFA at the IdP. +- temporal-tls (REASONED; temporal-rolling): Internode and frontend mTLS are separate from UI login and API authorization. +- temporal-payloads (REASONED; temporal-rolling, temporal-codec): Keep plaintext credentials out of persisted inputs/results/history; the Temporal Go SDK v1.28.0 codec HTTP handler accepts encode/decode POSTs without an authentication check, so authenticate and restrict a deployed Codec Server separately. +- temporal-ports (REASONED; temporal-rolling): Frontend gRPC uses 7233 and the Web UI 8233 in the cited CLI server reference. +- flower-bind (REASONED; flower, tornado): Flower defaults to wildcard address and 5555 unless Unix socket, FLOWER_ variables or implicit working-directory flowerconfig.py override; bind loopback explicitly. +- flower-api (REASONED; flower-rolling): Authentication defaults off; unauthenticated HTTP API is disabled unless FLOWER_UNAUTHENTICATED_API=true, which must not be mistaken for dashboard protection. +- flower-basic (REASONED; flower-rolling): Basic auth uses a comma-separated credential list; supply it through FLOWER_BASIC_AUTH or protected config rather than argv and retain private TLS fronting. +- flower-oauth (REASONED; flower-rolling): OAuth needs handler, client key/secret, redirect and allowed-email regex; protect the secret outside argv and prefer an MFA-enforcing provider. +- flower-socket (REASONED; flower, tornado, linux): Flower passes fixed mode 0777; Tornado removes/recreates filesystem sockets on startup, so chmod does not persist. Abstract sockets have no filesystem permission boundary. +- flower-directory (REASONED; linux): Restrict socket directory search with Flower ownership, proxy-only group, mode 0750 and no outsider ACL; directory access is essential because socket mode permits writes. +- flower-capabilities (REASONED; linux, docker): Applicable DAC capabilities bypass directory search checks; Docker defaults include CAP_DAC_OVERRIDE, while user namespaces and idmapped mounts change identity interpretation. +- flower-runtime-dir (REASONED; systemd, fhs): Reapply directory owner/group/mode on creation: /run is cleared at boot and RuntimeDirectory defaults 0755 unless RuntimeDirectoryMode overrides it. +- flower-numeric-ids (REASONED; linux): A proxy sharing the socket directory must hold the required numeric group after namespace/mount mappings; names alone do not establish access. +- argo-bind (REASONED; argo-three, argo-four): Argo Server binds wildcard :2746 with host-dependent IPv4/IPv6 support and no bind-address flag. +- argo-defaults (REASONED; argo-three, argo-four): Both tags default secure=true, auth-mode=client and hsts=true; flags and ARGO_ environment equivalents can override. +- argo-client (REASONED; argo-three, argo-four): Client mode uses caller Kubernetes credentials/RBAC; v4.1.4 validates via SelfSubjectReview and requires Kubernetes 1.28+. +- argo-quickstart (REASONED; argo-three, argo-four): Both tags' minimal/mysql/postgres quick-starts enable server plus client mode while retaining TLS; v4.1.4 telemetry does too. Anonymous callers inherit server permissions. +- argo-quickstart-rbac (REASONED; argo-three, argo-four): Quick-start binds cluster-wide workflow/template and Secret get/create permissions, plus cluster-workflow-template rights; it is demo-only. +- argo-installs (REASONED; argo-three, argo-four): Traced install inputs keep client auth/TLS with cluster-wide server RBAC; namespace-install adds --namespaced and namespace RBAC. Generated install YAML was absent. +- argo-service (REASONED; argo-three, argo-four): Base Service omits type, defaulting to ClusterIP, and maps 2746 to 2746; this does not authenticate reachable callers. +- argo-fallback (REASONED; argo-three, argo-four): Remove server auth mode: adding client/SSO does not prevent anonymous fallback to server credentials; local mode uses the server kubeconfig. +- argo-sso (REASONED; argo-docs, argo-three, argo-four): Enable sso.rbac.enabled and map groups to narrow ServiceAccounts through rbac-rule/precedence; without SSO RBAC, authenticated users share server permissions. +- argo-secrets (REASONED; argo-docs): Store SSO OAuth credentials in referenced Kubernetes Secrets and supply API tokens through protected input, not argv. +- argo-tls (REASONED; argo-three, argo-four): Keep TLS enabled with a trusted certificate Secret; no name generates a self-signed certificate, failed named-Secret loading fails startup, secure=false is plaintext and defeats HSTS. +- argo-metrics (REASONED; argo-three, argo-four): /metrics shares the server listener and auth gate unless ARGO_SERVER_METRICS_AUTH=false; server mode also admits anonymous metrics. +- argo-network (REASONED; argo-docs): Keep 2746 private behind authenticated HTTPS; restrict direct Pod/Service access and scope server, SSO and execution-account permissions. Workflow submission permits arbitrary containers unless constrained. +- verify-inventory (REASONED; flower, argo-three, iproute2, network-namespaces, prefect-rolling): ss inventories only the current namespace, not publication, routing or authentication; inspect publications and external reachability separately. +- verify-prefect (REASONED; prefect, prefect-source, prefect-rolling): Anonymous POST /api/flows/filter returning a JSON list is exposed; fixed is 401 with an authorized list on the same origin. Health/ready GET exemptions are version-dependent. +- verify-dagster (REASONED; dagster-rolling): A RepositoryConnection from /graphql is a read, including empty nodes; GraphQL/transport errors are inconclusive. Test proxy authentication and origin isolation separately. +- verify-airflow (REASONED; airflow-two, airflow-rolling, fab-rolling): FAB 3.9.0 returning 201 with access_token for airflow/airflow is exposed; pair a valid account and anonymous/authenticated API reads at proxy and origin. Airflow 2 uses its own API/auth. +- verify-temporal (REASONED; temporal-rolling): Credential-free workflow listing on 7233 is exposed; require rejection and a matched authorized call, not health, TLS errors or missing namespaces as proof of auth. +- verify-flower (REASONED; flower-rolling): Anonymous UI/API should yield Basic 401 or OAuth login redirect, with a valid-session control; API-disabled is not proof the dashboard is protected. +- verify-socket (REASONED; flower, linux): Directory/socket permissions, outsider denial, reboot persistence and container numeric IDs remain reasoned; no socket or Flower process ran. +- flower-stat-run (DEMONSTRATED; linux): Tmpfs directory-only stat distinguished 0755 and 0750; the 0750 directory initially had only base ACL entries. +- flower-acl-run (DEMONSTRATED; linux): A named nobody search ACL remained invisible in stat mode 750; ls and getfacl exposed the entry. +- flower-mask-run (DEMONSTRATED; linux): Changing the ACL mask hid named search permission; chmod 0750 restored its reported effectiveness. Access as nobody was not tested. +- verify-argo (REASONED; argo-three, argo-four, argo-docs): Anonymous workflow JSON is exposed when server RBAC permits; missing tokens should yield 401 without server mode. A 403 alone is inconclusive; pair authorized 200 and inspect auth modes. +- verify-origin (REASONED; airflow, argo-docs): Any HTTP response from the origin at an untrusted vantage proves publication; use the same responding allowed origin as control and treat connection errors/timeouts as inconclusive. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-10", + "body_sha256": "fa18bb5bca5189881826e754d7d45b7a5179143b73c35dd0f654f46b3e7945b1", + "components": { + "prefect": { + "name": "Prefect Basic Auth minimum", + "basis": "3.1.8", + "sources": { + "s12416d1a0d69": "https://github.com/PrefectHQ/prefect/releases/tag/3.1.8" + } + }, + "prefect-blocks": { + "name": "Prefect Blocks source", + "basis": "3.1.8", + "sources": { + "s56081101d16a": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/server/api/block_documents.py#L47-L70", + "s1eae2c5f9f5e": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/server/api/block_documents.py#L101-L119", + "s46e69a2090c4": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/blocks/system.py#L154-L159" + } + }, + "dagster": { + "name": "Dagster OSS", + "basis": "1.13.24", + "sources": { + "sab1b42d92f27": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L41-L42", + "s6bc57f86dfe1": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L81-L96", + "s2455602a504b": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L306-L311", + "s3c7d8f17f245": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster/dagster/_cli/dev.py#L251-L252", + "s469c458c4a71": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L339-L351", + "s695dfd19dcca": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/templates/helpers/_deployment-webserver.tpl#L86-L90", + "sf6f027e96a0d": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/templates/helpers/_helpers.tpl#L55", + "seda3a1b24425": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/values.yaml#L54-L58" + } + }, + "airflow": { + "name": "Apache Airflow", + "basis": "3.3.2", + "sources": { + "s1d0b86fe4334": "https://airflow.apache.org/docs/apache-airflow/3.3.2/security/security_model.html", + "sad02a49561a0": "https://airflow.apache.org/docs/apache-airflow/3.3.2/core-concepts/auth-manager/simple/index.html", + "s3be3e4616150": "https://airflow.apache.org/docs/apache-airflow/3.3.2/core-concepts/auth-manager/index.html", + "s78c661193a1a": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#simple-auth-manager-all-admins", + "s963b49f58a13": "https://airflow.apache.org/docs/apache-airflow/3.3.2/security/api.html", + "s705f1eb5de00": "https://raw.githubusercontent.com/apache/airflow/3.3.2/airflow-core/docs/howto/docker-compose/docker-compose.yaml", + "sdfcfd543928c": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#secret-key", + "s4ed64009ed8f": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#expose-config" + } + }, + "airflow-two": { + "name": "Apache Airflow historical configuration", + "basis": "2.11.0", + "sources": { + "sb97f3044e215": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#auth-backends", + "sabb71378ad74": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#secret-key", + "s6813801b7880": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#expose-config" + } + }, + "airflow-old": { + "name": "Apache Airflow historical default", + "basis": "2.2.5", + "sources": { + "s4e72b1f51038": "https://airflow.apache.org/docs/apache-airflow/2.2.5/configurations-ref.html#auth-backend" + } + }, + "fab-source": { + "name": "Apache Airflow FAB provider source", + "basis": "providers-fab/3.9.0", + "sources": { + "s3c94f0f7176a": "https://github.com/apache/airflow/blob/providers-fab/3.9.0/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py#L327-L381", + "s5552a28ab9d8": "https://github.com/apache/airflow/blob/providers-fab/3.9.0/providers/fab/pyproject.toml#L75-L81" + } + }, + "airflow-crypto": { + "name": "Apache Airflow crypto source", + "basis": "3.3.2", + "sources": { + "sdeaf2f54c054": "https://github.com/apache/airflow/blob/3.3.2/airflow-core/src/airflow/models/crypto.py#L47-L68", + "s70ea7631e820": "https://github.com/apache/airflow/blob/3.3.2/airflow-core/src/airflow/models/crypto.py#L154-L173" + } + }, + "temporal-codec": { + "name": "Temporal Go SDK codec source", + "basis": "v1.28.0", + "sources": { + "s24435f531da0": "https://github.com/temporalio/sdk-go/blob/v1.28.0/converter/codec.go#L284-L350" + } + }, + "flower": { + "name": "Flower", + "basis": "v2.2.0", + "sources": { + "s25760fae7152": "https://github.com/mher/flower/blob/v2.2.0/flower/options.py#L7-L15", + "s1f72a8415337": "https://github.com/mher/flower/blob/v2.2.0/flower/options.py#L56-L57", + "s4dd518442b7f": "https://github.com/mher/flower/blob/v2.2.0/flower/command.py#L39-L91", + "s4e49ebc71dc1": "https://github.com/mher/flower/blob/v2.2.0/flower/app.py#L71-L76", + "s0fe6c77e9c9d": "https://github.com/mher/flower/blob/v2.2.0/flower/app.py#L85-L89" + } + }, + "argo-three": { + "name": "Argo Workflows", + "basis": "v3.7.18", + "sources": { + "s262098553d2f": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/cmd/argo/commands/server.go#L191-L229", + "sbddb9f07a301": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/apiserver/argoserver.go#L263-L282", + "s2ca4f14c3164": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/gatekeeper.go#L166-L218", + "sfbb904619bfa": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/overlays/argo-server-deployment.yaml#L1-L15", + "s0ac72aa1a86c": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterole.yaml#L1-L65", + "s1689f25010be": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/cluster-workflow-template-rbac.yaml#L1-L58", + "sf0c3a52d3302": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/Makefile#L493-L515", + "sfdcdf614ee37": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/base/argo-server/argo-server-deployment.yaml#L14-L34", + "sc7ca3a37b16c": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/overlays/argo-server-deployment.yaml#L1-L4", + "s01f102470d97": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/base/argo-server/argo-server-service.yaml#L1-L11", + "s49cdaf0691c0": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/mode.go#L33-L44", + "sdb4a0c18b7c6": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/cmd/argo/commands/server.go#L113-L137", + "s36759aec55fa": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/apiserver/argoserver.go#L430-L449", + "s862033e52c9b": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-minimal.yaml#L3613-L3664", + "s73e9beeef775": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-mysql.yaml#L3678-L3729", + "s476ff15db454": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-postgres.yaml#L3677-L3728", + "sa4b2fa875915": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterolebinding.yaml#L1-L11", + "s48db5ba07d23": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/kustomization.yaml#L4-L5", + "se3ffdf2973a6": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/argo-server-rbac/argo-server-role.yaml#L1-L65", + "sb901e7f87b54": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/argo-server-rbac/argo-server-rolebinding.yaml#L1-L11", + "s49a8855b6da2": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/docs/argo-server-auth-mode.md#L3-L9", + "s64e74b0dbe95": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/gatekeeper.go#L106-L121" + } + }, + "argo-four": { + "name": "Argo Workflows", + "basis": "v4.1.4", + "sources": { + "s774215022573": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/cmd/argo/commands/server.go#L195-L239", + "s0f074a9ac21e": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/apiserver/argoserver.go#L299-L320", + "sb284f93cde9c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/gatekeeper.go#L189-L244", + "s0871a1dd9b13": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/docs/argo-server-auth-mode.md#L3-L9", + "sa7b9db8384be": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start/base/overlays/argo-server-deployment.yaml#L1-L15", + "s79648b46d3f5": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-telemetry.yaml#L192919-L192970", + "sd2706d04971d": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterole.yaml#L1-L67", + "sd920da4ad4b7": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/docs/quick-start.md#L3-L34", + "s144b824252c1": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/Makefile#L608-L634", + "s51645bfbc47c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/base/argo-server/argo-server-deployment.yaml#L14-L34", + "s5188f6f2efbb": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/overlays/argo-server-deployment.yaml#L1-L4", + "s639e9fe0024e": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/base/argo-server/argo-server-service.yaml#L1-L11", + "s4613c15c72af": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/mode.go#L33-L44", + "s0b16f1ac50df": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/cmd/argo/commands/server.go#L119-L143", + "sa120f74372eb": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/apiserver/argoserver.go#L476-L494", + "s6c08a5f34f2c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-minimal.yaml#L192772-L192823", + "sa0bf6187d1db": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-mysql.yaml#L192838-L192889", + "s31f4a149576b": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-postgres.yaml#L192837-L192888", + "s3be9b03e29be": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterolebinding.yaml#L1-L11", + "sc263bd937cf0": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start/base/kustomization.yaml#L4-L5", + "s1b134f96428f": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/argo-server-rbac/argo-server-role.yaml#L1-L67", + "s2418ee1aa49d": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/argo-server-rbac/argo-server-rolebinding.yaml#L1-L11", + "se758d83aaf1f": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/gatekeeper.go#L133-L148" + } + }, + "argo-docs": { + "name": "Argo Workflows documentation", + "basis": "release-3.7", + "sources": { + "s6c8e78288632": "https://argo-workflows.readthedocs.io/en/release-3.7/security/", + "s9898d4653a47": "https://argo-workflows.readthedocs.io/en/release-3.7/argo-server-sso/", + "s96145eacf345": "https://argo-workflows.readthedocs.io/en/release-3.7/rest-examples/" + } + }, + "prefect-source": { + "name": "Prefect server source", + "basis": "9e560c9b6df4e19a5109a66e66d461f9facb538d", + "sources": { + "sa4574c7ed80f": "https://github.com/PrefectHQ/prefect/blob/9e560c9b6df4e19a5109a66e66d461f9facb538d/src/prefect/server/api/server.py" + } + }, + "prefect-rolling": { + "name": "Prefect documentation (rolling)", + "basis": "unknown", + "sources": { + "sbe64d51110a2": "https://docs.prefect.io/v3/how-to-guides/self-hosted/server-cli", + "s0ef73a227667": "https://docs.prefect.io/v3/advanced/security-settings" + } + }, + "dagster-rolling": { + "name": "Dagster documentation (rolling)", + "basis": "unknown", + "sources": { + "s4c04d5da5257": "https://docs.dagster.io/guides/operate/webserver" + } + }, + "airflow-rolling": { + "name": "Apache Airflow documentation (rolling)", + "basis": "unknown", + "sources": { + "s2d1891641c91": "https://airflow.apache.org/docs/apache-airflow/stable/security/", + "s39bfd91c507b": "https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html" + } + }, + "fab-rolling": { + "name": "Apache Airflow FAB provider documentation (rolling)", + "basis": "unknown", + "sources": { + "s989f09d05161": "https://airflow.apache.org/docs/apache-airflow-providers-fab/stable/auth-manager/api-authentication.html" + } + }, + "temporal-rolling": { + "name": "Temporal documentation (rolling)", + "basis": "unknown", + "sources": { + "s12f2c64ad396": "https://docs.temporal.io/cli/command-reference/server", + "s06d4f0dbd0c8": "https://docs.temporal.io/self-hosted-guide/security", + "sa9d5614385ca": "https://docs.temporal.io/references/web-ui-configuration" + } + }, + "flower-rolling": { + "name": "Flower documentation (rolling)", + "basis": "unknown", + "sources": { + "sb1ba69f79c9b": "https://flower.readthedocs.io/en/latest/config.html" + } + }, + "tornado": { + "name": "Tornado", + "basis": "v6.5.10", + "sources": { + "sc503b4425831": "https://github.com/tornadoweb/tornado/blob/v6.5.10/tornado/netutil.py#L72-L73", + "s737c51cccd00": "https://github.com/tornadoweb/tornado/blob/v6.5.10/tornado/netutil.py#L215-L228" + } + }, + "docker": { + "name": "Moby Docker", + "basis": "docker-v29.8.1", + "sources": { + "s1f268e247a1f": "https://github.com/moby/moby/blob/docker-v29.8.1/daemon/pkg/oci/caps/defaults.go#L4-L7" + } + }, + "linux": { + "name": "Linux permissions and tools", + "basis": "unknown", + "sources": { + "s8ffd93b0efa4": "https://man7.org/linux/man-pages/man7/unix.7.html", + "sfb53d2819cd0": "https://man7.org/linux/man-pages/man7/path_resolution.7.html", + "s31f41c54750c": "https://man7.org/linux/man-pages/man5/acl.5.html", + "s3cf3d5be0f64": "https://man7.org/linux/man-pages/man7/user_namespaces.7.html", + "sa5ccbfea6e16": "https://www.kernel.org/doc/html/latest/filesystems/idmappings.html", + "s08902025e5f9": "https://man7.org/linux/man-pages/man1/getfacl.1.html", + "sa0d3793fabb7": "https://man7.org/linux/man-pages/man1/stat.1.html" + } + }, + "systemd": { + "name": "systemd", + "basis": "v257", + "sources": { + "s48ece26c16d3": "https://github.com/systemd/systemd/blob/v257/man/systemd.exec.xml#L1630-L1640" + } + }, + "fhs": { + "name": "Filesystem Hierarchy Standard", + "basis": "3.0", + "sources": { + "seafd8219f367": "https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch03s15.html" + } + }, + "iproute2": { + "name": "iproute2 ss manual", + "basis": "v6.15.0", + "sources": { + "s6cc7f285a0c7": "https://github.com/iproute2/iproute2/blob/v6.15.0/man/man8/ss.8#L358-L359", + "s9fa9a260f1de": "https://github.com/iproute2/iproute2/blob/v6.15.0/man/man8/ss.8#L7-L12" + } + }, + "network-namespaces": { + "name": "Linux network namespaces manual", + "basis": "man-pages-5.13", + "sources": { + "s427c923e346f": "https://github.com/mkerrisk/man-pages/blob/man-pages-5.13/man7/network_namespaces.7#L30-L40" + } + } + }, + "claims": { + "fronting": {"text": "Keep UI/API origins private behind HTTPS and authentication; use IdP MFA for browsers and separate machine authentication, and constrain worker egress.", "components": ["airflow", "argo-docs", "temporal-rolling"], "sources": ["airflow:s1d0b86fe4334", "temporal-rolling:s06d4f0dbd0c8", "argo-docs:s6c8e78288632"], "status": "REASONED"}, + "prefect-auth": {"text": "Self-hosted Prefect defaults unauthenticated; Basic Auth from 3.1.8 uses matching server/client auth strings and prompts the UI.", "components": ["prefect", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, + "prefect-key": {"text": "Cloud-only PREFECT_API_KEY takes precedence over PREFECT_API_AUTH_STRING and produces 401 against a self-hosted server.", "components": ["prefect", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, + "prefect-secrets": {"text": "Protect the auth string and credential-bearing Blocks; Prefect 3.1.8 block-document read routes accept include_secrets, and Secret.get() returns the underlying value. Restrict API and worker access; UI masking does not enforce authorization.", "components": ["prefect-blocks", "prefect-rolling"], "sources": ["prefect-blocks:s56081101d16a", "prefect-blocks:s1eae2c5f9f5e", "prefect-blocks:s46e69a2090c4", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, + "prefect-port": {"text": "The self-hosted Prefect server uses port 4200.", "components": ["prefect-rolling"], "sources": ["prefect-rolling:sbe64d51110a2"], "status": "REASONED"}, + "dagster-bind": {"text": "Direct dagster-webserver/dev defaults to 127.0.0.1:3000, with flag/environment overrides and free-port fallback when the port is unset or 0; explicitly bind the private origin.", "components": ["dagster"], "sources": ["dagster:sab1b42d92f27", "dagster:s6bc57f86dfe1", "dagster:s2455602a504b", "dagster:s3c7d8f17f245", "dagster:s469c458c4a71"], "status": "REASONED"}, + "dagster-helm": {"text": "The chart passes -h 0.0.0.0 on the Service port, default 80, with ClusterIP Service by default.", "components": ["dagster"], "sources": ["dagster:s695dfd19dcca", "dagster:sf6f027e96a0d", "dagster:seda3a1b24425"], "status": "REASONED"}, + "dagster-auth": {"text": "OSS webserver has no built-in login or access control; protect all routes with an authenticating proxy.", "components": ["dagster-rolling"], "sources": ["dagster-rolling:s4c04d5da5257"], "status": "REASONED"}, + "airflow-boundary": {"text": "Airflow assumes authenticated known users and is not designed for untrusted public exposure; deployment managers must keep it private.", "components": ["airflow"], "sources": ["airflow:s1d0b86fe4334"], "status": "REASONED"}, + "airflow-two-api": {"text": "Airflow 2.11.0 uses [api] auth_backends with session default; before 2.3 the name was auth_backend and 2.2.5 defaulted to deny_all.", "components": ["airflow-two", "airflow-old"], "sources": ["airflow-two:sb97f3044e215", "airflow-old:s4e72b1f51038"], "status": "REASONED"}, + "airflow-simple": {"text": "Airflow 3 defaults to development-only Simple Auth Manager, with configured users/roles and generated passwords printed to logs unless supplied.", "components": ["airflow"], "sources": ["airflow:sad02a49561a0", "airflow:s3be3e4616150"], "status": "REASONED"}, + "airflow-fab": {"text": "For production install FAB and select FabAuthManager through [core] auth_manager; verify the effective manager and use an identity backend with MFA.", "components": ["airflow", "fab-rolling"], "sources": ["airflow:s3be3e4616150", "fab-rolling:s989f09d05161"], "status": "REASONED"}, + "airflow-api": {"text": "Airflow 3 public API uses JWT independently of [fab] auth_backends, which selects FAB API backends rather than the auth manager.", "components": ["airflow", "fab-rolling"], "sources": ["airflow:s3be3e4616150", "fab-rolling:s989f09d05161", "airflow:s963b49f58a13"], "status": "REASONED"}, + "airflow-all-admins": {"text": "Keep simple_auth_manager_all_admins unset or False; enabling it disables login and grants every visitor admin.", "components": ["airflow"], "sources": ["airflow:sad02a49561a0", "airflow:s78c661193a1a"], "status": "REASONED"}, + "fab-public-role": {"text": "Leave [fab] auth_role_public and legacy AUTH_ROLE_PUBLIC unset; a configured public role can make FAB treat an anonymous visitor as logged in and supply that role's permissions.", "components": ["fab-source"], "sources": ["fab-source:s3c94f0f7176a", "fab-source:s5552a28ab9d8"], "status": "REASONED"}, + "airflow-compose-account": {"text": "Development Compose selects FAB but defaults to airflow/airflow; set credentials before account creation and update/delete an existing account separately.", "components": ["airflow-rolling", "fab-rolling", "airflow"], "sources": ["airflow-rolling:s39bfd91c507b", "fab-rolling:s989f09d05161", "airflow:s705f1eb5de00"], "status": "REASONED"}, + "airflow-jwt": {"text": "Replace Compose fallback AIRFLOW__API_AUTH__JWT_SECRET=airflow_jwt_secret and share the strong value with all signers/validators; the public fallback permits token forgery.", "components": ["airflow-rolling", "airflow"], "sources": ["airflow-rolling:s39bfd91c507b", "airflow:s705f1eb5de00"], "status": "REASONED"}, + "airflow-secret-key": {"text": "Provision the independent secret_key too: [webserver] on 2.11.0 and [api] on 3.3.2.", "components": ["airflow-two", "airflow-rolling", "airflow"], "sources": ["airflow-rolling:s2d1891641c91", "airflow-two:sabb71378ad74", "airflow:sdfcfd543928c"], "status": "REASONED"}, + "airflow-config": {"text": "Keep configuration exposure off with WEBSERVER__EXPOSE_CONFIG on 2.11.0 or API__EXPOSE_CONFIG on 3.3.2.", "components": ["airflow-two", "airflow-rolling", "airflow"], "sources": ["airflow-rolling:s2d1891641c91", "airflow-two:s6813801b7880", "airflow:s4ed64009ed8f"], "status": "REASONED"}, + "airflow-fernet": {"text": "Protect Connections, Variables, database and Fernet keys; an empty [core] FERNET_KEY selects a no-encryption implementation for new values, while configured comma-separated keys use MultiFernet. Preserve keys needed to decrypt existing values; authorized workloads still use secrets.", "components": ["airflow-crypto", "airflow-rolling"], "sources": ["airflow-crypto:sdeaf2f54c054", "airflow-crypto:s70ea7631e820", "airflow-rolling:s2d1891641c91"], "status": "REASONED"}, + "airflow-port": {"text": "Compose publishes 8080 on all interfaces; use 127.0.0.1:8080:8080 behind the fronting layer.", "components": ["airflow-rolling"], "sources": ["airflow-rolling:s39bfd91c507b"], "status": "REASONED"}, + "temporal-authorizer": {"text": "Empty authorizer selects noopAuthorizer allowing every API request, including administration; configure default authorization with trusted JWT keys and audience.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, + "temporal-claims": {"text": "Empty claimMapper selects a no-op granting system-admin claims; configure both Authorizer and ClaimMapper, with frontend TLS.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, + "temporal-health": {"text": "The built-in default authorizer permits health checks without claims; health is not an authorization discriminator.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, + "temporal-ui": {"text": "UI OIDC auth.enabled is a sibling of providers; TEMPORAL_AUTH_ENABLED plus provider settings gates the UI only, with MFA at the IdP.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:sa9d5614385ca"], "status": "REASONED"}, + "temporal-tls": {"text": "Internode and frontend mTLS are separate from UI login and API authorization.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, + "temporal-payloads": {"text": "Keep plaintext credentials out of persisted inputs/results/history; the Temporal Go SDK v1.28.0 codec HTTP handler accepts encode/decode POSTs without an authentication check, so authenticate and restrict a deployed Codec Server separately.", "components": ["temporal-rolling", "temporal-codec"], "sources": ["temporal-rolling:s06d4f0dbd0c8", "temporal-codec:s24435f531da0"], "status": "REASONED"}, + "temporal-ports": {"text": "Frontend gRPC uses 7233 and the Web UI 8233 in the cited CLI server reference.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s12f2c64ad396"], "status": "REASONED"}, + "flower-bind": {"text": "Flower defaults to wildcard address and 5555 unless Unix socket, FLOWER_ variables or implicit working-directory flowerconfig.py override; bind loopback explicitly.", "components": ["flower", "tornado"], "sources": ["flower:s25760fae7152", "flower:s1f72a8415337", "flower:s4dd518442b7f", "flower:s4e49ebc71dc1", "tornado:sc503b4425831"], "status": "REASONED"}, + "flower-api": {"text": "Authentication defaults off; unauthenticated HTTP API is disabled unless FLOWER_UNAUTHENTICATED_API=true, which must not be mistaken for dashboard protection.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, + "flower-basic": {"text": "Basic auth uses a comma-separated credential list; supply it through FLOWER_BASIC_AUTH or protected config rather than argv and retain private TLS fronting.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, + "flower-oauth": {"text": "OAuth needs handler, client key/secret, redirect and allowed-email regex; protect the secret outside argv and prefer an MFA-enforcing provider.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, + "flower-socket": {"text": "Flower passes fixed mode 0777; Tornado removes/recreates filesystem sockets on startup, so chmod does not persist. Abstract sockets have no filesystem permission boundary.", "components": ["flower", "tornado", "linux"], "sources": ["flower:s0fe6c77e9c9d", "tornado:s737c51cccd00", "linux:s8ffd93b0efa4"], "status": "REASONED"}, + "flower-directory": {"text": "Restrict socket directory search with Flower ownership, proxy-only group, mode 0750 and no outsider ACL; directory access is essential because socket mode permits writes.", "components": ["linux"], "sources": ["linux:sfb53d2819cd0", "linux:s8ffd93b0efa4", "linux:s31f41c54750c"], "status": "REASONED"}, + "flower-capabilities": {"text": "Applicable DAC capabilities bypass directory search checks; Docker defaults include CAP_DAC_OVERRIDE, while user namespaces and idmapped mounts change identity interpretation.", "components": ["linux", "docker"], "sources": ["linux:s3cf3d5be0f64", "linux:sa5ccbfea6e16", "docker:s1f268e247a1f"], "status": "REASONED"}, + "flower-runtime-dir": {"text": "Reapply directory owner/group/mode on creation: /run is cleared at boot and RuntimeDirectory defaults 0755 unless RuntimeDirectoryMode overrides it.", "components": ["systemd", "fhs"], "sources": ["systemd:s48ece26c16d3", "fhs:seafd8219f367"], "status": "REASONED"}, + "flower-numeric-ids": {"text": "A proxy sharing the socket directory must hold the required numeric group after namespace/mount mappings; names alone do not establish access.", "components": ["linux"], "sources": ["linux:s3cf3d5be0f64", "linux:sa5ccbfea6e16"], "status": "REASONED"}, + "argo-bind": {"text": "Argo Server binds wildcard :2746 with host-dependent IPv4/IPv6 support and no bind-address flag.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s262098553d2f", "argo-four:s774215022573", "argo-three:sbddb9f07a301", "argo-four:s0f074a9ac21e"], "status": "REASONED"}, + "argo-defaults": {"text": "Both tags default secure=true, auth-mode=client and hsts=true; flags and ARGO_ environment equivalents can override.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s262098553d2f", "argo-four:s774215022573"], "status": "REASONED"}, + "argo-client": {"text": "Client mode uses caller Kubernetes credentials/RBAC; v4.1.4 validates via SelfSubjectReview and requires Kubernetes 1.28+.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c", "argo-four:s0871a1dd9b13"], "status": "REASONED"}, + "argo-quickstart": {"text": "Both tags' minimal/mysql/postgres quick-starts enable server plus client mode while retaining TLS; v4.1.4 telemetry does too. Anonymous callers inherit server permissions.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sfbb904619bfa", "argo-four:sa7b9db8384be", "argo-four:s79648b46d3f5", "argo-three:s862033e52c9b", "argo-three:s73e9beeef775", "argo-three:s476ff15db454", "argo-three:s262098553d2f", "argo-three:s49cdaf0691c0", "argo-three:s2ca4f14c3164", "argo-four:s6c08a5f34f2c", "argo-four:sa0bf6187d1db", "argo-four:s31f4a149576b", "argo-four:s774215022573", "argo-four:s4613c15c72af", "argo-four:sb284f93cde9c"], "status": "REASONED"}, + "argo-quickstart-rbac": {"text": "Quick-start binds cluster-wide workflow/template and Secret get/create permissions, plus cluster-workflow-template rights; it is demo-only.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s0ac72aa1a86c", "argo-four:sd2706d04971d", "argo-three:s1689f25010be", "argo-four:sd920da4ad4b7", "argo-three:sa4b2fa875915", "argo-three:s48db5ba07d23", "argo-four:s3be9b03e29be", "argo-four:sc263bd937cf0"], "status": "REASONED"}, + "argo-installs": {"text": "Traced install inputs keep client auth/TLS with cluster-wide server RBAC; namespace-install adds --namespaced and namespace RBAC. Generated install YAML was absent.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sf0c3a52d3302", "argo-four:s144b824252c1", "argo-three:sfdcdf614ee37", "argo-four:s51645bfbc47c", "argo-three:sc7ca3a37b16c", "argo-four:s5188f6f2efbb", "argo-three:s262098553d2f", "argo-three:s0ac72aa1a86c", "argo-three:sa4b2fa875915", "argo-three:se3ffdf2973a6", "argo-three:sb901e7f87b54", "argo-four:s774215022573", "argo-four:sd2706d04971d", "argo-four:s3be9b03e29be", "argo-four:s1b134f96428f", "argo-four:s2418ee1aa49d"], "status": "REASONED"}, + "argo-service": {"text": "Base Service omits type, defaulting to ClusterIP, and maps 2746 to 2746; this does not authenticate reachable callers.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s01f102470d97", "argo-four:s639e9fe0024e"], "status": "REASONED"}, + "argo-fallback": {"text": "Remove server auth mode: adding client/SSO does not prevent anonymous fallback to server credentials; local mode uses the server kubeconfig.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s49cdaf0691c0", "argo-four:s4613c15c72af", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c", "argo-four:s0871a1dd9b13", "argo-three:s49a8855b6da2"], "status": "REASONED"}, + "argo-sso": {"text": "Enable sso.rbac.enabled and map groups to narrow ServiceAccounts through rbac-rule/precedence; without SSO RBAC, authenticated users share server permissions.", "components": ["argo-docs", "argo-three", "argo-four"], "sources": ["argo-docs:s9898d4653a47", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c"], "status": "REASONED"}, + "argo-secrets": {"text": "Store SSO OAuth credentials in referenced Kubernetes Secrets and supply API tokens through protected input, not argv.", "components": ["argo-docs"], "sources": ["argo-docs:s9898d4653a47", "argo-docs:s96145eacf345"], "status": "REASONED"}, + "argo-tls": {"text": "Keep TLS enabled with a trusted certificate Secret; no name generates a self-signed certificate, failed named-Secret loading fails startup, secure=false is plaintext and defeats HSTS.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sdb4a0c18b7c6", "argo-four:s0b16f1ac50df", "argo-three:s36759aec55fa", "argo-four:sa120f74372eb"], "status": "REASONED"}, + "argo-metrics": {"text": "/metrics shares the server listener and auth gate unless ARGO_SERVER_METRICS_AUTH=false; server mode also admits anonymous metrics.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s36759aec55fa", "argo-four:sa120f74372eb", "argo-three:s49cdaf0691c0", "argo-three:s2ca4f14c3164", "argo-four:s4613c15c72af", "argo-four:sb284f93cde9c", "argo-three:s64e74b0dbe95", "argo-four:se758d83aaf1f"], "status": "REASONED"}, + "argo-network": {"text": "Keep 2746 private behind authenticated HTTPS; restrict direct Pod/Service access and scope server, SSO and execution-account permissions. Workflow submission permits arbitrary containers unless constrained.", "components": ["argo-docs"], "sources": ["argo-docs:s6c8e78288632"], "status": "REASONED"}, + "verify-inventory": {"text": "ss inventories only the current namespace, not publication, routing or authentication; inspect publications and external reachability separately.", "components": ["flower", "argo-three", "iproute2", "network-namespaces", "prefect-rolling"], "sources": ["prefect-rolling:sbe64d51110a2", "flower:s25760fae7152", "argo-three:sbddb9f07a301", "iproute2:s6cc7f285a0c7", "network-namespaces:s427c923e346f", "iproute2:s9fa9a260f1de"], "status": "REASONED", "verify": [1]}, + "verify-prefect": {"text": "Anonymous POST /api/flows/filter returning a JSON list is exposed; fixed is 401 with an authorized list on the same origin. Health/ready GET exemptions are version-dependent.", "components": ["prefect", "prefect-source", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-source:sa4574c7ed80f", "prefect-rolling:s0ef73a227667"], "status": "REASONED", "verify": [1]}, + "verify-dagster": {"text": "A RepositoryConnection from /graphql is a read, including empty nodes; GraphQL/transport errors are inconclusive. Test proxy authentication and origin isolation separately.", "components": ["dagster-rolling"], "sources": ["dagster-rolling:s4c04d5da5257"], "status": "REASONED", "verify": [1]}, + "verify-airflow": {"text": "FAB 3.9.0 returning 201 with access_token for airflow/airflow is exposed; pair a valid account and anonymous/authenticated API reads at proxy and origin. Airflow 2 uses its own API/auth.", "components": ["airflow-two", "airflow-rolling", "fab-rolling"], "sources": ["fab-rolling:s989f09d05161", "airflow-rolling:s39bfd91c507b", "airflow-two:sb97f3044e215"], "status": "REASONED", "verify": [1]}, + "verify-temporal": {"text": "Credential-free workflow listing on 7233 is exposed; require rejection and a matched authorized call, not health, TLS errors or missing namespaces as proof of auth.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8", "temporal-rolling:s12f2c64ad396"], "status": "REASONED", "verify": [1]}, + "verify-flower": {"text": "Anonymous UI/API should yield Basic 401 or OAuth login redirect, with a valid-session control; API-disabled is not proof the dashboard is protected.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED", "verify": [1]}, + "verify-socket": {"text": "Directory/socket permissions, outsider denial, reboot persistence and container numeric IDs remain reasoned; no socket or Flower process ran.", "components": ["flower", "linux"], "sources": ["flower:s0fe6c77e9c9d", "linux:s31f41c54750c", "linux:s08902025e5f9", "linux:sa0d3793fabb7"], "status": "REASONED", "verify": [1]}, + "flower-stat-run": {"text": "Tmpfs directory-only stat distinguished 0755 and 0750; the 0750 directory initially had only base ACL entries.", "components": ["linux"], "sources": ["linux:sa0d3793fabb7", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "`stat -c '%a %U:%G %u:%g %n'` printed `755` for a directory created with mode `0755` and `750` for one created with `0750`; `getfacl -p` on the `0750` directory listed only `user::rwx`, `group::r-x` and `other::---`."}, + "flower-acl-run": {"text": "A named nobody search ACL remained invisible in stat mode 750; ls and getfacl exposed the entry.", "components": ["linux"], "sources": ["linux:s31f41c54750c", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "After `setfacl -m u:nobody:--x` on the `0750` directory, `stat` still printed `750`. Only the trailing `+` in `ls -ld` and getfacl's `user:nobody:--x` line showed the named entry."}, + "flower-mask-run": {"text": "Changing the ACL mask hid named search permission; chmod 0750 restored its reported effectiveness. Access as nobody was not tested.", "components": ["linux"], "sources": ["linux:s31f41c54750c", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "After `setfacl -m m::r--`, getfacl printed `user:nobody:--x` with `#effective:---`. While masked, `stat` printed `740`."}, + "verify-argo": {"text": "Anonymous workflow JSON is exposed when server RBAC permits; missing tokens should yield 401 without server mode. A 403 alone is inconclusive; pair authorized 200 and inspect auth modes.", "components": ["argo-three", "argo-four", "argo-docs"], "sources": ["argo-three:s49cdaf0691c0", "argo-four:s4613c15c72af", "argo-docs:s96145eacf345", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c"], "status": "REASONED", "verify": [1]}, + "verify-origin": {"text": "Any HTTP response from the origin at an untrusted vantage proves publication; use the same responding allowed origin as control and treat connection errors/timeouts as inconclusive.", "components": ["airflow", "argo-docs"], "sources": ["airflow:s1d0b86fe4334", "argo-docs:s6c8e78288632"], "status": "REASONED", "verify": [1]} + } +} +```` + + ====================================================================== ==> admin-uis.md ====================================================================== @@ -41812,7 +44786,7 @@ REASONED: preceding block; listener inventory, anonymous front-page/data-route c # Self-hosted observability components: node_exporter, Alertmanager, Pushgateway, Jaeger, and Loki -Written against node_exporter v1.12.1, Alertmanager v0.34.1, Pushgateway v1.11.3, Jaeger v2.21.0, Loki v3.7.8, Exporter toolkit v0.17.1, OpenTelemetry configtls v1.66.0, and OpenTelemetry basicauth extension v0.160.0, plus 1 more recorded bases; 3 further sources are pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 59 claims: 14 demonstrated by a check that ran and 45 reasoned from cited sources. +Written against node_exporter v1.12.1, Alertmanager v0.34.1, Pushgateway v1.11.3, Jaeger v2.21.0, Loki v3.7.8, Exporter toolkit v0.17.1, OpenTelemetry configtls v1.66.0, and OpenTelemetry basicauth extension v0.160.0, plus 1 more recorded basis; 3 further sources are pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 59 claims: 14 demonstrated by a check that ran and 45 reasoned from cited sources. A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. @@ -42963,36 +45937,9 @@ REASONED: preceding block; listener and panel-login checks, Docker 2375 refusal # CORS: allow your origins, not everyone's - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-09-27; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- protocol: MDN CORS documentation (unknown) -- express: Express cors middleware (unknown) -- fastapi: FastAPI CORSMiddleware (unknown) -- fetch: Fetch Standard credentials (unknown) - -Claims (full source mapping in the front matter): +Written against MDN CORS documentation (no recorded version); 3 further sources carry no recorded version. Metadata reviewed 2026-09-27; documentation checked September 2026. The guide records 13 claims, all reasoned from cited sources. -- exact-origins (REASONED; protocol): Allow exact browser origins, illustrated by Access-Control-Allow-Origin: https://app.example.com; keep environment-specific lists in configuration instead of production localhost entries. -- wildcard-credentials (REASONED; protocol): Browsers refuse wildcard Access-Control-Allow-Origin with credentials; the guide restricts wildcard use to public, unauthenticated, read-only resources. -- origin-reflection (REASONED; protocol): Reflect only origins checked against an explicit allowlist; reflecting arbitrary Origin values permits hostile sites to read credentialed responses. -- authentication (REASONED; protocol, express): CORS controls browsers rather than curl or other attackers, does not expose a port and does not replace endpoint authentication. -- express-origins (REASONED; express): Express cors uses origin: ['https://app.example.com'] as the allowed origin list. -- express-credentials (REASONED; express): The Express example enables credentials with credentials: true. -- fastapi-origins (REASONED; fastapi): FastAPI adds CORSMiddleware with allow_origins restricted to https://app.example.com. -- fastapi-credentials (REASONED; fastapi): The FastAPI example enables allow_credentials=True. -- fastapi-methods (REASONED; fastapi): The FastAPI example allows only GET and POST through allow_methods. -- fastapi-headers (REASONED; fastapi): The FastAPI example allows Authorization and Content-Type through allow_headers. -- verify-hostile (REASONED; protocol, express, fastapi, fetch): The exposed API echoes https://evil.example in Access-Control-Allow-Origin; the fixed API omits that hostile origin. No deployed API or browser run is recorded. -- verify-allowed (REASONED; protocol, express, fastapi, fetch): The allowed-Origin probe should return the listed app origin, with Allow-Credentials only where the browser must send cookies, HTTP authentication or TLS client certificates; no actual response is recorded. -- credential-types (REASONED; fetch, protocol): Browser credentials include cookies, HTTP authentication and TLS client certificates; Access-Control-Allow-Credentials: true is needed only where the browser must send credentials. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. CORS misconfiguration does not expose a port; it lets hostile websites use your users' browsers, cookies included, against your API. AI assistants reach for `Access-Control-Allow-Origin: *` the moment a browser console shows a CORS error; that is the wrong fix for any API that authenticates. @@ -43048,59 +45995,122 @@ Keep the origin list in configuration per environment rather than hardcoding loc - Express `cors` middleware, for the `origin` and `credentials` options used above: https://expressjs.com/en/resources/middleware/cors/ - FastAPI CORS, for `CORSMiddleware` and its parameters: https://fastapi.tiangolo.com/tutorial/cors/ -====================================================================== -==> headers.md -====================================================================== - -# Security headers for your application +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/cors.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + **Version basis** -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. -Metadata reviewed 2026-10-17; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- mdn: MDN HTTP documentation (unknown) -- nginx-source: nginx source (release-1.26.3) -- apache-source: Apache HTTP Server (2.4.69) -- caddy-source: Caddy source (v2.11.4) -- cloudflare-pages: Cloudflare Pages documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- scanner: Security Headers scanner (unknown) -- helmet: Helmet (v8.1.0) -- django: Django (5.2) -- curl: curl (8.23.0) +- protocol: MDN CORS documentation (unknown) +- fetch: Fetch Standard credentials (unknown) +- express: Express cors middleware (unknown) +- fastapi: FastAPI CORSMiddleware (unknown) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- placement (REASONED; mdn, helmet, django, nginx-source, apache-source, caddy-source, cloudflare-pages): Set response headers at a proxy with nginx add_header, Apache Header or Caddy header, in application middleware, or in a Cloudflare Pages _headers file for static asset responses; Pages Functions responses will likely need headers set in Functions code. Applying the three proxy directives to all six listed headers is this guide's inference. -- hsts (REASONED; mdn): Use HSTS max-age=31536000 with includeSubDomains only after HTTPS works across the domain; every subdomain is committed to HTTPS and max-age=0 disables HSTS. -- hsts-preload (REASONED; mdn): Omit preload unless its commitment is understood; removal is slow, takes weeks and depends on browser list updates. -- csp (REASONED; mdn): Start CSP at default-src 'self', tailor sources to the app and prefer script nonces or hashes over unsafe-inline; Verify requires an enforcing policy. -- csp-report-only (REASONED; mdn): Roll out Content-Security-Policy-Report-Only first on existing apps to find breakage; it enforces nothing and is not a substitute for enforcing CSP. -- nosniff (REASONED; mdn): Send X-Content-Type-Options: nosniff and verify that value on the final response. -- referrer (REASONED; mdn): Send Referrer-Policy: strict-origin-when-cross-origin and verify that final-response value. -- permissions (REASONED; mdn): Permissions-Policy disables camera, microphone and geolocation with empty allowlists; verify all three restrictions. -- framing (REASONED; mdn): X-Frame-Options: DENY or enforcing CSP frame-ancestors 'none' denies framing; supported frame-ancestors supersedes X-Frame-Options, and default-src does not cover it. frame-ancestors * overrides DENY. -- all-responses (REASONED; mdn, nginx-source): Send headers on every response, including errors; nginx add_header inherits parent headers only when the child has no header list, and always covers statuses outside its default set. This guide assesses setting headers only on 200 / as a common proxy misconfiguration. -- shared-cache (REASONED; mdn): Use Cache-Control: private for authenticated responses or no-store for sensitive ones; cache only public content unless authenticated caching is explicitly configured. -- cache-key (REASONED; mdn): A URL-only shared cache can leak one user's response to another; explicitly vary authenticated cache entries on the cookie or authorization header. -- browser-cache (REASONED; mdn): private still permits browser storage; no-store forbids private and shared storage and belongs on authenticated responses and responses setting session cookies. -- history-cache (REASONED; mdn): no-cache does not prevent a Back navigation from restoring a back/forward-cache snapshot without revalidation. -- logout-cache (REASONED; mdn): Send Clear-Site-Data on logout confirmation; cache clears browser cache and may clear back/forward cache depending on the browser. -- logout-cookies (REASONED; mdn): Clear-Site-Data cookies clears cookies across the registered domain and all subdomains, plus HTTP authentication credentials; invalidate the server session separately. -- logout-storage (REASONED; mdn): Clear-Site-Data storage removes origin local/session storage, IndexedDB and service-worker registrations; understand effects on companion applications before clearing. -- logout-header (REASONED; mdn): The cited MDN logout example includes cache, cookies, storage, executionContexts, prefetchCache and prerenderCache. -- verify-fetch (REASONED; mdn, curl): Fetch the final page with redirects restricted to HTTPS, bypass client proxies, discard the body and inspect the last response's headers and final status/URL; curl 8.23.0 documents the command options and write-out fields, while `-D -` printing each response's header block, and selecting the last one, are this guide's assessment. -- verify-values (REASONED; mdn): Judge header values on the final response, not presence on an earlier hop; missing final headers are findings and a failed fetch is inconclusive. -- verify-scanner (REASONED; scanner): Use the external Security Headers scanner for a second opinion; the guide states that it fetches with GET. -- verify-csp (REASONED; mdn): Unexpected console violations must be absent in normal use, but this alone is insufficient; confirm a deliberately disallowed resource is blocked while an allowed one loads. -- verify-cache (REASONED; mdn): Warm an authenticated URL in the shared cache, then request it as user A, user B and anonymously; each response must belong only to its caller. -- verify-logout (REASONED; mdn): After logout, Back must not restore authenticated content; verify a request to the login screen and inspect no-store and Clear-Site-Data separately in every supported browser. +- exact-origins (REASONED; protocol): Allow exact browser origins, illustrated by Access-Control-Allow-Origin: https://app.example.com; keep environment-specific lists in configuration instead of production localhost entries. +- wildcard-credentials (REASONED; protocol): Browsers refuse wildcard Access-Control-Allow-Origin with credentials; the guide restricts wildcard use to public, unauthenticated, read-only resources. +- origin-reflection (REASONED; protocol): Reflect only origins checked against an explicit allowlist; reflecting arbitrary Origin values permits hostile sites to read credentialed responses. +- authentication (REASONED; protocol, express): CORS controls browsers rather than curl or other attackers, does not expose a port and does not replace endpoint authentication. +- express-origins (REASONED; express): Express cors uses origin: ['https://app.example.com'] as the allowed origin list. +- express-credentials (REASONED; express): The Express example enables credentials with credentials: true. +- fastapi-origins (REASONED; fastapi): FastAPI adds CORSMiddleware with allow_origins restricted to https://app.example.com. +- fastapi-credentials (REASONED; fastapi): The FastAPI example enables allow_credentials=True. +- fastapi-methods (REASONED; fastapi): The FastAPI example allows only GET and POST through allow_methods. +- fastapi-headers (REASONED; fastapi): The FastAPI example allows Authorization and Content-Type through allow_headers. +- verify-hostile (REASONED; protocol, express, fastapi, fetch): The exposed API echoes https://evil.example in Access-Control-Allow-Origin; the fixed API omits that hostile origin. No deployed API or browser run is recorded. +- verify-allowed (REASONED; protocol, express, fastapi, fetch): The allowed-Origin probe should return the listed app origin, with Allow-Credentials only where the browser must send cookies, HTTP authentication or TLS client certificates; no actual response is recorded. +- credential-types (REASONED; fetch, protocol): Browser credentials include cookies, HTTP authentication and TLS client certificates; Access-Control-Allow-Credentials: true is needed only where the browser must send credentials. + +````json +{ + "schema": 1, + "checked": "2026-09-27", + "documentation_checked": "2026-09", + "body_sha256": "1ab4078e43ca8a5f60f429011121b3a68578a9252ac5d688dfbcd65f7b1ad6cd", + "components": { + "protocol": { + "name": "MDN CORS documentation", + "basis": "unknown", + "sources": { + "s6265cf028385": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS" + } + }, + "fetch": { + "name": "Fetch Standard credentials", + "basis": "unknown", + "sources": { + "s028c38ebd452": "https://fetch.spec.whatwg.org/#credentials" + } + }, + "express": { + "name": "Express cors middleware", + "basis": "unknown", + "sources": { + "sb6448b9a443d": "https://expressjs.com/en/resources/middleware/cors/" + } + }, + "fastapi": { + "name": "FastAPI CORSMiddleware", + "basis": "unknown", + "sources": { + "s4f1cea92e7bc": "https://fastapi.tiangolo.com/tutorial/cors/" + } + } + }, + "claims": { + "exact-origins": {"text": "Allow exact browser origins, illustrated by Access-Control-Allow-Origin: https://app.example.com; keep environment-specific lists in configuration instead of production localhost entries.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, + "wildcard-credentials": {"text": "Browsers refuse wildcard Access-Control-Allow-Origin with credentials; the guide restricts wildcard use to public, unauthenticated, read-only resources.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, + "origin-reflection": {"text": "Reflect only origins checked against an explicit allowlist; reflecting arbitrary Origin values permits hostile sites to read credentialed responses.", "components": ["protocol"], "sources": ["protocol:s6265cf028385"], "status": "REASONED"}, + "authentication": {"text": "CORS controls browsers rather than curl or other attackers, does not expose a port and does not replace endpoint authentication.", "components": ["protocol", "express"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d"], "status": "REASONED"}, + "express-origins": {"text": "Express cors uses origin: ['https://app.example.com'] as the allowed origin list.", "components": ["express"], "sources": ["express:sb6448b9a443d"], "status": "REASONED"}, + "express-credentials": {"text": "The Express example enables credentials with credentials: true.", "components": ["express"], "sources": ["express:sb6448b9a443d"], "status": "REASONED"}, + "fastapi-origins": {"text": "FastAPI adds CORSMiddleware with allow_origins restricted to https://app.example.com.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, + "fastapi-credentials": {"text": "The FastAPI example enables allow_credentials=True.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, + "fastapi-methods": {"text": "The FastAPI example allows only GET and POST through allow_methods.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, + "fastapi-headers": {"text": "The FastAPI example allows Authorization and Content-Type through allow_headers.", "components": ["fastapi"], "sources": ["fastapi:s4f1cea92e7bc"], "status": "REASONED"}, + "verify-hostile": {"text": "The exposed API echoes https://evil.example in Access-Control-Allow-Origin; the fixed API omits that hostile origin. No deployed API or browser run is recorded.", "components": ["protocol", "express", "fastapi", "fetch"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d", "fastapi:s4f1cea92e7bc", "fetch:s028c38ebd452"], "status": "REASONED", "verify": [1]}, + "verify-allowed": {"text": "The allowed-Origin probe should return the listed app origin, with Allow-Credentials only where the browser must send cookies, HTTP authentication or TLS client certificates; no actual response is recorded.", "components": ["protocol", "express", "fastapi", "fetch"], "sources": ["protocol:s6265cf028385", "express:sb6448b9a443d", "fastapi:s4f1cea92e7bc", "fetch:s028c38ebd452"], "status": "REASONED", "verify": [1]}, + "credential-types": {"text": "Browser credentials include cookies, HTTP authentication and TLS client certificates; Access-Control-Allow-Credentials: true is needed only where the browser must send credentials.", "components": ["fetch", "protocol"], "sources": ["fetch:s028c38ebd452", "protocol:s6265cf028385"], "status": "REASONED"} + } +} +```` +====================================================================== +==> headers.md +====================================================================== + +# Security headers for your application + +Written against MDN HTTP documentation (no recorded version), nginx source release-1.26.3, Apache HTTP Server 2.4.69, Caddy source v2.11.4, Helmet v8.1.0, Django 5.2, and curl 8.23.0; 1 further source is pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 24 claims, all reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + TLS protects the transport; these response headers protect the page. Set them at the proxy ([`add_header`](https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L110-L116) in [nginx.md](nginx.md), [`Header`](https://github.com/apache/httpd/blob/2.4.69/docs/manual/mod/mod_headers.xml#L311-L328) in [apache.md](apache.md), [`header`](https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/headers/caddyfile.go#L27-L48) in Caddy), in app middleware (helmet for Express per [nodejs.md](nodejs.md), Django's security settings per [python.md](python.md)), or in a [`_headers` file](https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L14) for Cloudflare Pages static asset responses. Cloudflare Pages does not apply `_headers` to Pages Functions responses; set those headers in [Functions code](https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L26). Applying the three proxy directives to all six headers below is this guide's inference. ## The set worth shipping @@ -43200,102 +46210,212 @@ REASONED: logout, browser history and response-header inspection, per the guide' - curl 8.23.0 --output short form -o (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/output.md#L6 - curl 8.23.0 --write-out short form -w (checked October 2026): https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L5 -====================================================================== -==> firebase-supabase.md -====================================================================== - -# Firebase and Supabase: the rules are the security +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/headers.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + **Version basis** -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- firebase: Firebase documentation (unknown) -- supabase: Supabase documentation (unknown) -- postgres: PostgreSQL documentation (unknown) -- postgres-view: PostgreSQL invoker-view minimum (15) -- curl: curl (curl-8_14_1) -- shellcheck: ShellCheck (v0.11.0) +- mdn: MDN HTTP documentation (unknown) +- nginx-source: nginx source (release-1.26.3) +- apache-source: Apache HTTP Server (2.4.69) +- caddy-source: Caddy source (v2.11.4) +- cloudflare-pages: Cloudflare Pages documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- helmet: Helmet (v8.1.0) +- django: Django (5.2) +- scanner: Security Headers scanner (unknown) +- curl: curl (8.23.0) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- public-config (REASONED; firebase, supabase): Firebase API keys/config and Supabase publishable/anon keys are public identifiers, not authorization; hosted endpoints handle TLS. -- firebase-ownership (REASONED; firebase): Require authentication and ownership separately in Firestore, RTDB and Storage; authentication-only rules do not isolate users. -- firebase-starting (REASONED; firebase): Firestore production and RTDB locked modes deny clients; test templates expose data for about a month. Inspect expiry and replace open rules. -- firestore-rules (REASONED; firebase): Rules version 2 recursive wildcards include the user document and descendants; version 1 excludes the parent. Unmatched paths deny and overlapping grants can reopen access. -- rtdb-rules (REASONED; firebase): Owner rules under users.$uid need ancestor grants removed; child rules cannot revoke parent grants or filter parent reads. -- firebase-storage (REASONED; firebase): Storage has separate rules; conflicting default prose/example warrants explicit deny-all followed by deliberate owner-path access. -- firebase-download (REASONED; firebase): Storage download URLs are non-expiring bearer credentials unaffected by tighter Rules; revoke tokens and use authorized getBlob/getBytes direct downloads. -- firebase-rule-deploy (REASONED; firebase): Inspect and test deployed rules for every database/bucket/instance; keep local and deployed definitions synchronized. -- firebase-server (REASONED; firebase): Admin SDK/server credentials bypass client Rules; keep credentials server-side and apply IAM and caller authorization. -- firebase-callable (REASONED; firebase): Callable ID-token validation is optional authentication; reject missing request.auth, use its UID and validate resource access. First-generation handlers use context.auth. -- firebase-callable-appcheck (REASONED; firebase): onCall enforceAppCheck needs firebase-functions 4.0.0 or newer; App Check does not replace caller/resource authorization. -- firebase-http (REASONED; firebase): onRequest does not inherit callable auth; verify end-user tokens with verifyIdToken or restrict IAM invokers, then authorize the intended principal. -- firebase-appcheck (REASONED; firebase): Register compatible clients and inspect metrics, then enforce App Check per product and per callable; registration alone enforces nothing. -- firebase-key-restrictions (REASONED; firebase): Apply one app-restriction type per key plus API restrictions; separate platforms and unrelated billable APIs, using the full Firebase product table. -- firebase-anonymous (REASONED; firebase): Anonymous sign-in is opt-in; disable unused providers because anonymous accounts still satisfy authentication-only Rules. -- firebase-enumeration (REASONED; firebase): Email enumeration protection defaults on only for projects created from September 15, 2023; signup can still return EMAIL_EXISTS. -- firebase-emulators (REASONED; firebase): Emulators default to localhost but missing Firestore/RTDB/Storage Rules configuration means open data; keep listeners, UI, hub, proxies and tunnels private. -- supabase-network (REASONED; supabase): Restrict hosted database/pooler client IPs with Network Restrictions, enforce SSL and protect database-role passwords. -- supabase-grants (REASONED; supabase): Data API access needs grants plus RLS; automatic-grant defaults are transitioning. Table Editor enables RLS, while SQL creation needs explicit enablement. -- supabase-select (REASONED; supabase, postgres): The profiles baseline enables RLS, revokes broad grants and permits authenticated owners SELECT; audit inherited/column grants and existing policies. -- supabase-bypass (REASONED; postgres): With RLS enabled and no policy, ordinary subject roles get no rows; owners normally bypass, as do superusers/BYPASSRLS roles. TRUNCATE is outside RLS. -- supabase-writes (REASONED; supabase, postgres): Grant and policy INSERT/UPDATE/DELETE only when intended; UPDATE checks old/new ownership and needs applicable SELECT when reading columns. -- supabase-schemas (REASONED; supabase): public is exposed by default; restrict exposed schemas and deliberately grant schema/object access. Schema selection does not replace RLS. -- supabase-api-disable (REASONED; supabase): Disable an unused Data API to close generated REST endpoints; this does not disable all other services. -- supabase-keys (REASONED; supabase): Publishable keys select anon or authenticated by session; secret and legacy service_role keys bypass RLS and belong only on trusted servers. -- supabase-key-retire (REASONED; supabase): Replacement keys do not disable legacy keys; migrate consumers then explicitly deactivate legacy keys, or delete compromised secret keys. -- supabase-jwt-retire (REASONED; supabase): Rotation changes the signing key but retains previous trust until revocation; account for verifier caches and propagation. -- supabase-view-owner (REASONED; postgres, supabase): Views use owner privileges/policies by default; disclosure depends on reachable schemas, grants, query and owner privileges, not anon-key possession alone. -- supabase-view-invoker (REASONED; postgres-view, postgres): PostgreSQL 15+ security_invoker views apply caller policies and require underlying privileges. -- supabase-view-legacy (REASONED; postgres): PostgreSQL 14 and earlier require revoking all view privileges from PUBLIC/anon/authenticated or unexposing the schema; updatable views can bypass RLS for writes. -- supabase-functions (REASONED; supabase, postgres): Prefer INVOKER; restrict DEFINER execution, use empty search_path with qualified references and enforce caller authorization including required MFA. -- supabase-function-defaults (REASONED; postgres): New functions grant PUBLIC execution; create privileged functions and restrict grants transactionally. Global/default revocations are creator-specific and do not fix existing functions. -- supabase-http (REASONED; supabase): Audit http/pg_net wrappers and revoke unneeded execute grants; constrain callers/destinations/data. Extension enablement alone does not prove exploitable SSRF. -- supabase-vault (REASONED; supabase): Restrict vault.decrypted_secrets and privileged wrappers and keep Vault unexposed; at-rest encryption cannot prevent authorized plaintext queries. -- supabase-buckets (REASONED; supabase): Private downloads need authorized access or time-limited signed URLs; public buckets bypass retrieval checks but not upload/delete/move/copy controls. -- supabase-storage-policies (REASONED; supabase): Authorize storage.objects separately by bucket and owner path; upload needs INSERT, upsert also SELECT/UPDATE, and deletion needs deliberate permission. -- supabase-realtime-default (REASONED; supabase): Public Realtime channels are allowed by default; disable them project-wide and also set client config.private. -- supabase-realtime-policy (REASONED; supabase): Broadcast/Presence receive/send need separate SELECT/INSERT policies on realtime.messages, constrained by topic ownership and extension. -- supabase-changes (REASONED; supabase): Postgres Changes uses source-table authorization; private-channel policies are not universal and DELETE has separate limitations. -- supabase-signup (REASONED; supabase): Disable signup, anonymous sign-ins and unused providers when unwanted; inspect hosted settings instead of assuming universal defaults. -- supabase-cli-auth (REASONED; supabase): CLI enable_signup defaults true and enable_anonymous_sign_ins false; local config edits do not prove hosted settings changed. -- supabase-mfa (REASONED; supabase, postgres): TOTP is available on every plan at writing; enrollment alone does not constrain data. Require aal2 using a restrictive policy alongside permissive ownership policies. -- supabase-edge-jwt (REASONED; supabase): Per-function verify_jwt defaults true; --no-verify-jwt disables platform checking and requires handler authentication before protected work. -- supabase-edge-auth (REASONED; supabase): Platform JWT success is not resource authorization and API-key compatibility can pass it; separately authorize users or verify webhook signatures. -- supabase-edge-secrets (REASONED; supabase): Edge environments contain privileged credentials including SUPABASE_SERVICE_ROLE_KEY; protect managed/local secrets, responses, logs and client bundles. -- verify-firestore (REASONED; firebase): Compare unsigned/A/B reads and writes to user documents and descendants, with a successful owner control; test parent documents to distinguish recursive-rule versions. -- verify-rtdb (REASONED; firebase): Compare unsigned/A/B reads/writes to seeded RTDB owner paths; ancestor grants expose them while ownership rules deny unauthorized callers. -- verify-firebase-storage (REASONED; firebase): Compare unsigned/A/B object reads and uploads; ownership rules must deny unrelated callers while owner operations succeed. -- verify-download-token (REASONED; firebase): Same sessionless download URL works after Rules tightening but must lose bytes after token revocation; owner getBytes remains successful and network/CORS failures are inconclusive. -- verify-tables (REASONED; supabase, postgres, curl): Pair unsigned/A/B profiles GETs against seeded rows, using ordinary credentials and required MFA; test intended writes and persisted effects separately. -- verify-views (REASONED; postgres): Compare view reads with table boundaries; owner-privileged exposure must close under invoker/revoked access while intended reads or deliberate removal are confirmed. -- verify-rpc (REASONED; supabase): Test every API function overload with valid harmless arguments as unsigned/A/B; unauthorized data or operations must be denied while the intended caller succeeds. -- verify-http (REASONED; supabase): Probe HTTP wrappers with a controlled collector and marker; unauthorized fixed calls must emit no request. Commit pg_net fixture transactions before expecting delivery. -- verify-vault (REASONED; supabase, postgres): Use a disposable Vault canary to compare exposed/fixed wrappers; inspect function and table privileges as supporting evidence. -- verify-storage (REASONED; supabase): Compare public URL and A/B private downloads plus intended uploads/upserts; unauthorized callers must receive no bytes or cross-owner writes. -- verify-realtime (REASONED; supabase): Compare A/B join, Broadcast and Presence delivery; public-disabled clients lacking private=true must fail, and connection alone is insufficient. -- verify-changes (REASONED; supabase): Observe A/B INSERT/UPDATE subscription events against seeded rows; fixed table authorization withholds unrelated events. Evaluate DELETE separately. -- verify-edge (REASONED; supabase): Compare no/invalid Bearer without apikey, authorized owners and valid webhook signatures; distinguish platform 401 from handler authorization and inspect effects/secrets. -- verify-firebase-functions (REASONED; firebase): Compare callable requests with valid App Check but missing user auth and A/B operations; HTTP endpoints need separate invalid/valid user or IAM comparisons. -- verify-appcheck (REASONED; firebase): Repeat owner requests with/without App Check per product/callable; enforcement must deny missing attestation while compatible clients work. -- verify-api-restrictions (REASONED; firebase): Test intended versus excluded apps/referrers after key restrictions, retaining successful intended product traffic. -- verify-emulators (REASONED; firebase): Enumerate hub /emulators and sockets, compare missing versus loaded Rules and second-host connectivity, and retain local owner controls. -- verify-firebase-accounts (REASONED; firebase): Sign out before each Anonymous signup test and check isNewUser; disabled providers must reject creation. Enumeration tests compare sign-in errors, not EMAIL_EXISTS signup behavior. -- verify-supabase-accounts (REASONED; supabase): Fresh signup and anonymous creation must fail under invite-only/anonymous-disabled settings while an invited account still signs in. -- verify-mfa (REASONED; supabase): Owner profiles GET with aal1 must yield no protected rows under the restrictive policy, while fresh aal2 returns the seeded owner row. -- verify-key-retirement (REASONED; supabase): Compare old/new API keys after explicit deactivation and old/new JWTs after revocation, accounting for propagation and caches. -- verify-api-removal (REASONED; supabase): Previously successful profiles requests must stop returning fixtures after schema removal or Data API disabling; retained schemas need authorized controls. -- verify-bundle (REASONED; supabase): Filename-only service_role/eyJ/sb_secret_ scanning yields review candidates, not proof of leaks; public anon JWTs are legitimate. Use a marker control and inspect encoded/split secrets separately. -- local-guards (DEMONSTRATED; supabase, shellcheck): Historical local guard tests rejected placeholders and malformed set lines; this guide's assessment is shell behavior only, not live authorization. The recorded ShellCheck 0.11.0 has a release citation, not host-binary evidence; bash and other tool/parser versions remain unrecorded. -- local-syntax (DEMONSTRATED; firebase, supabase, shellcheck): Recorded ShellCheck 0.11.0/bash and JSON/TOML/JavaScript parsing passed; this guide's assessment is that these checks do not compile Firebase Rules, execute SQL or demonstrate services. Only the ShellCheck release is cited; bash and other tool/parser versions remain unrecorded. +- placement (REASONED; mdn, helmet, django, nginx-source, apache-source, caddy-source, cloudflare-pages): Set response headers at a proxy with nginx add_header, Apache Header or Caddy header, in application middleware, or in a Cloudflare Pages _headers file for static asset responses; Pages Functions responses will likely need headers set in Functions code. Applying the three proxy directives to all six listed headers is this guide's inference. +- hsts (REASONED; mdn): Use HSTS max-age=31536000 with includeSubDomains only after HTTPS works across the domain; every subdomain is committed to HTTPS and max-age=0 disables HSTS. +- hsts-preload (REASONED; mdn): Omit preload unless its commitment is understood; removal is slow, takes weeks and depends on browser list updates. +- csp (REASONED; mdn): Start CSP at default-src 'self', tailor sources to the app and prefer script nonces or hashes over unsafe-inline; Verify requires an enforcing policy. +- csp-report-only (REASONED; mdn): Roll out Content-Security-Policy-Report-Only first on existing apps to find breakage; it enforces nothing and is not a substitute for enforcing CSP. +- nosniff (REASONED; mdn): Send X-Content-Type-Options: nosniff and verify that value on the final response. +- referrer (REASONED; mdn): Send Referrer-Policy: strict-origin-when-cross-origin and verify that final-response value. +- permissions (REASONED; mdn): Permissions-Policy disables camera, microphone and geolocation with empty allowlists; verify all three restrictions. +- framing (REASONED; mdn): X-Frame-Options: DENY or enforcing CSP frame-ancestors 'none' denies framing; supported frame-ancestors supersedes X-Frame-Options, and default-src does not cover it. frame-ancestors * overrides DENY. +- all-responses (REASONED; mdn, nginx-source): Send headers on every response, including errors; nginx add_header inherits parent headers only when the child has no header list, and always covers statuses outside its default set. This guide assesses setting headers only on 200 / as a common proxy misconfiguration. +- shared-cache (REASONED; mdn): Use Cache-Control: private for authenticated responses or no-store for sensitive ones; cache only public content unless authenticated caching is explicitly configured. +- cache-key (REASONED; mdn): A URL-only shared cache can leak one user's response to another; explicitly vary authenticated cache entries on the cookie or authorization header. +- browser-cache (REASONED; mdn): private still permits browser storage; no-store forbids private and shared storage and belongs on authenticated responses and responses setting session cookies. +- history-cache (REASONED; mdn): no-cache does not prevent a Back navigation from restoring a back/forward-cache snapshot without revalidation. +- logout-cache (REASONED; mdn): Send Clear-Site-Data on logout confirmation; cache clears browser cache and may clear back/forward cache depending on the browser. +- logout-cookies (REASONED; mdn): Clear-Site-Data cookies clears cookies across the registered domain and all subdomains, plus HTTP authentication credentials; invalidate the server session separately. +- logout-storage (REASONED; mdn): Clear-Site-Data storage removes origin local/session storage, IndexedDB and service-worker registrations; understand effects on companion applications before clearing. +- logout-header (REASONED; mdn): The cited MDN logout example includes cache, cookies, storage, executionContexts, prefetchCache and prerenderCache. +- verify-fetch (REASONED; mdn, curl): Fetch the final page with redirects restricted to HTTPS, bypass client proxies, discard the body and inspect the last response's headers and final status/URL; curl 8.23.0 documents the command options and write-out fields, while `-D -` printing each response's header block, and selecting the last one, are this guide's assessment. +- verify-values (REASONED; mdn): Judge header values on the final response, not presence on an earlier hop; missing final headers are findings and a failed fetch is inconclusive. +- verify-scanner (REASONED; scanner): Use the external Security Headers scanner for a second opinion; the guide states that it fetches with GET. +- verify-csp (REASONED; mdn): Unexpected console violations must be absent in normal use, but this alone is insufficient; confirm a deliberately disallowed resource is blocked while an allowed one loads. +- verify-cache (REASONED; mdn): Warm an authenticated URL in the shared cache, then request it as user A, user B and anonymously; each response must belong only to its caller. +- verify-logout (REASONED; mdn): After logout, Back must not restore authenticated content; verify a request to the login screen and inspect no-store and Clear-Site-Data separately in every supported browser. + +````json +{ + "schema": 1, + "checked": "2026-10-17", + "documentation_checked": "2026-09", + "body_sha256": "e17c3af4fb92d40391435139ae145152d8827211b3ff71dbcec2a3a642b8eb67", + "components": { + "mdn": { + "name": "MDN HTTP documentation", + "basis": "unknown", + "sources": { + "s40155272e6bf": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers", + "s9166d803cf3a": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control", + "s7f290c45ad37": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Clear-Site-Data", + "s46957d9dc1d5": "https://developer.mozilla.org/en-US/docs/Glossary/bfcache" + } + }, + "nginx-source": { + "name": "nginx source", + "basis": "release-1.26.3", + "sources": { + "s24b0a72bc894": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L110-L116", + "s0ae4e9bc8d07": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L216-L218", + "sedd333de1a8c": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L665-L690", + "sf6eac8f6749d": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L788-L796", + "s70d54f325a93": "https://github.com/nginx/nginx/blob/release-1.26.3/src/http/modules/ngx_http_headers_filter_module.c#L843-L849" + } + }, + "apache-source": { + "name": "Apache HTTP Server", + "basis": "2.4.69", + "sources": { + "s2f07cea83845": "https://github.com/apache/httpd/blob/2.4.69/docs/manual/mod/mod_headers.xml#L311-L328" + } + }, + "caddy-source": { + "name": "Caddy source", + "basis": "v2.11.4", + "sources": { + "s0fa8b969b205": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/headers/caddyfile.go#L27-L48" + } + }, + "cloudflare-pages": { + "name": "Cloudflare Pages documentation", + "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "s38b6aa1f9566": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L14", + "sba79d4023eca": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/pages/configuration/headers.mdx#L26" + } + }, + "helmet": { + "name": "Helmet", + "basis": "v8.1.0", + "sources": { + "sf42121cfc014": "https://github.com/helmetjs/helmet/blob/v8.1.0/README.md#L3" + } + }, + "django": { + "name": "Django", + "basis": "5.2", + "sources": { + "se79954c08919": "https://docs.djangoproject.com/en/5.2/ref/middleware/#module-django.middleware.security" + } + }, + "scanner": { + "name": "Security Headers scanner", + "basis": "unknown", + "sources": { + "s7a94d0fcb6ad": "https://securityheaders.com/" + } + }, + "curl": { + "name": "curl", + "basis": "8.23.0", + "sources": { + "s2278b86c08a6": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/disable.md#L18-L20", + "s3b9e19f23f2d": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/globoff.md#L19-L22", + "s38391c89b58d": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/silent.md#L20-L25", + "sc1670c4672b4": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/show-error.md#L19", + "s891e2566ea41": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/location.md#L23-L26", + "sd386dc0317da": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/proto-redir.md#L19-L20", + "sf7e7834afa89": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/proto.md#L30-L33", + "s508454c45cf1": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/noproxy.md#L18-L23", + "s62990c766776": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/dump-header.md#L21-L23", + "seb2e6bc373fe": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/output.md#L53-L55", + "s3aefa5f74fd4": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L20-L29", + "se50d33437ccf": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L112-L114", + "sc8658f8a2e89": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/disable.md#L5", + "s3dbfd02b9aa2": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/globoff.md#L5", + "s5b5b25c9a3fe": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/silent.md#L5", + "s24da6d0f30ba": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/show-error.md#L5", + "s6bee713841f0": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/location.md#L5", + "se89c7271eda2": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/dump-header.md#L5", + "s93be9b76f8e2": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/output.md#L6", + "s3633dd0e6b3e": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L5", + "s9a3d23bc00be": "https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/write-out.md#L358-L360" + } + } + }, + "claims": { + "placement": {"text": "Set response headers at a proxy with nginx add_header, Apache Header or Caddy header, in application middleware, or in a Cloudflare Pages _headers file for static asset responses; Pages Functions responses will likely need headers set in Functions code. Applying the three proxy directives to all six listed headers is this guide's inference.", "components": ["mdn", "helmet", "django", "nginx-source", "apache-source", "caddy-source", "cloudflare-pages"], "sources": ["mdn:s40155272e6bf", "helmet:sf42121cfc014", "django:se79954c08919", "nginx-source:s24b0a72bc894", "nginx-source:sf6eac8f6749d", "apache-source:s2f07cea83845", "caddy-source:s0fa8b969b205", "cloudflare-pages:s38b6aa1f9566", "cloudflare-pages:sba79d4023eca"], "status": "REASONED"}, + "hsts": {"text": "Use HSTS max-age=31536000 with includeSubDomains only after HTTPS works across the domain; every subdomain is committed to HTTPS and max-age=0 disables HSTS.", "components": ["mdn"], "sources": ["mdn:s40155272e6bf"], "status": "REASONED", "verify": [1]}, + "hsts-preload": {"text": "Omit preload unless its commitment is understood; removal is slow, takes weeks and depends on browser list updates.", "components": ["mdn"], "sources": ["mdn:s40155272e6bf"], "status": "REASONED"}, + "csp": {"text": "Start CSP at default-src 'self', tailor sources to the app and prefer script nonces or hashes over unsafe-inline; Verify requires an enforcing policy.", "components": ["mdn"], "sources": ["mdn:s40155272e6bf"], "status": "REASONED", "verify": [1]}, + "csp-report-only": {"text": "Roll out Content-Security-Policy-Report-Only first on existing apps to find breakage; it enforces nothing and is not a substitute for enforcing CSP.", "components": ["mdn"], "sources": ["mdn:s40155272e6bf"], "status": "REASONED", "verify": [1]}, + "nosniff": {"text": "Send X-Content-Type-Options: nosniff and verify that value on the final response.", "components": ["mdn"], "sources": ["mdn:s40155272e6bf"], "status": "REASONED", "verify": [1]}, + "referrer": {"text": "Send Referrer-Policy: strict-origin-when-cross-origin and verify that final-response value.", "components": ["mdn"], "sources": ["mdn:s40155272e6bf"], "status": "REASONED", "verify": [1]}, + "permissions": {"text": "Permissions-Policy disables camera, microphone and geolocation with empty allowlists; verify all three restrictions.", "components": ["mdn"], "sources": ["mdn:s40155272e6bf"], "status": "REASONED", "verify": [1]}, + "framing": {"text": "X-Frame-Options: DENY or enforcing CSP frame-ancestors 'none' denies framing; supported frame-ancestors supersedes X-Frame-Options, and default-src does not cover it. frame-ancestors * overrides DENY.", "components": ["mdn"], "sources": ["mdn:s40155272e6bf"], "status": "REASONED", "verify": [1]}, + "all-responses": {"text": "Send headers on every response, including errors; nginx add_header inherits parent headers only when the child has no header list, and always covers statuses outside its default set. This guide assesses setting headers only on 200 / as a common proxy misconfiguration.", "components": ["mdn", "nginx-source"], "sources": ["mdn:s40155272e6bf", "nginx-source:sedd333de1a8c", "nginx-source:sf6eac8f6749d", "nginx-source:s0ae4e9bc8d07", "nginx-source:s70d54f325a93"], "status": "REASONED"}, + "shared-cache": {"text": "Use Cache-Control: private for authenticated responses or no-store for sensitive ones; cache only public content unless authenticated caching is explicitly configured.", "components": ["mdn"], "sources": ["mdn:s9166d803cf3a"], "status": "REASONED"}, + "cache-key": {"text": "A URL-only shared cache can leak one user's response to another; explicitly vary authenticated cache entries on the cookie or authorization header.", "components": ["mdn"], "sources": ["mdn:s9166d803cf3a", "mdn:s40155272e6bf"], "status": "REASONED"}, + "browser-cache": {"text": "private still permits browser storage; no-store forbids private and shared storage and belongs on authenticated responses and responses setting session cookies.", "components": ["mdn"], "sources": ["mdn:s9166d803cf3a"], "status": "REASONED"}, + "history-cache": {"text": "no-cache does not prevent a Back navigation from restoring a back/forward-cache snapshot without revalidation.", "components": ["mdn"], "sources": ["mdn:s9166d803cf3a", "mdn:s46957d9dc1d5"], "status": "REASONED"}, + "logout-cache": {"text": "Send Clear-Site-Data on logout confirmation; cache clears browser cache and may clear back/forward cache depending on the browser.", "components": ["mdn"], "sources": ["mdn:s7f290c45ad37", "mdn:s46957d9dc1d5"], "status": "REASONED"}, + "logout-cookies": {"text": "Clear-Site-Data cookies clears cookies across the registered domain and all subdomains, plus HTTP authentication credentials; invalidate the server session separately.", "components": ["mdn"], "sources": ["mdn:s7f290c45ad37"], "status": "REASONED"}, + "logout-storage": {"text": "Clear-Site-Data storage removes origin local/session storage, IndexedDB and service-worker registrations; understand effects on companion applications before clearing.", "components": ["mdn"], "sources": ["mdn:s7f290c45ad37"], "status": "REASONED"}, + "logout-header": {"text": "The cited MDN logout example includes cache, cookies, storage, executionContexts, prefetchCache and prerenderCache.", "components": ["mdn"], "sources": ["mdn:s7f290c45ad37"], "status": "REASONED"}, + "verify-fetch": {"text": "Fetch the final page with redirects restricted to HTTPS, bypass client proxies, discard the body and inspect the last response's headers and final status/URL; curl 8.23.0 documents the command options and write-out fields, while `-D -` printing each response's header block, and selecting the last one, are this guide's assessment.", "components": ["mdn", "curl"], "sources": ["mdn:s40155272e6bf", "curl:s2278b86c08a6", "curl:s3b9e19f23f2d", "curl:s38391c89b58d", "curl:sc1670c4672b4", "curl:s891e2566ea41", "curl:sd386dc0317da", "curl:sf7e7834afa89", "curl:s508454c45cf1", "curl:s62990c766776", "curl:seb2e6bc373fe", "curl:s3aefa5f74fd4", "curl:se50d33437ccf", "curl:s9a3d23bc00be", "curl:sc8658f8a2e89", "curl:s3dbfd02b9aa2", "curl:s5b5b25c9a3fe", "curl:s24da6d0f30ba", "curl:s6bee713841f0", "curl:se89c7271eda2", "curl:s93be9b76f8e2", "curl:s3633dd0e6b3e"], "status": "REASONED", "verify": [1]}, + "verify-values": {"text": "Judge header values on the final response, not presence on an earlier hop; missing final headers are findings and a failed fetch is inconclusive.", "components": ["mdn"], "sources": ["mdn:s40155272e6bf"], "status": "REASONED", "verify": [1]}, + "verify-scanner": {"text": "Use the external Security Headers scanner for a second opinion; the guide states that it fetches with GET.", "components": ["scanner"], "sources": ["scanner:s7a94d0fcb6ad"], "status": "REASONED"}, + "verify-csp": {"text": "Unexpected console violations must be absent in normal use, but this alone is insufficient; confirm a deliberately disallowed resource is blocked while an allowed one loads.", "components": ["mdn"], "sources": ["mdn:s40155272e6bf"], "status": "REASONED"}, + "verify-cache": {"text": "Warm an authenticated URL in the shared cache, then request it as user A, user B and anonymously; each response must belong only to its caller.", "components": ["mdn"], "sources": ["mdn:s9166d803cf3a"], "status": "REASONED"}, + "verify-logout": {"text": "After logout, Back must not restore authenticated content; verify a request to the login screen and inspect no-store and Clear-Site-Data separately in every supported browser.", "components": ["mdn"], "sources": ["mdn:s9166d803cf3a", "mdn:s7f290c45ad37", "mdn:s46957d9dc1d5"], "status": "REASONED"} + } +} +```` +====================================================================== +==> firebase-supabase.md +====================================================================== + +# Firebase and Supabase: the rules are the security + +Written against Firebase documentation (no recorded version), PostgreSQL invoker-view minimum 15, curl curl-8_14_1, and ShellCheck v0.11.0; 2 further sources carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 70 claims: 2 demonstrated by a check that ran and 68 reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + These platforms handle TLS for you on their hosted endpoints; the exposure works differently. Client SDKs talk to the backend using keys that ship in your frontend code and are **public by design**: the Firebase API key and Supabase publishable or legacy `anon` key. Client data access @@ -44245,6 +47365,315 @@ REASONED: tool-version provenance for the historical local runs above; the [Shel - [curl curl-8_14_1 headers from stdin (checked October 2026)](https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md). - [ShellCheck v0.11.0 changelog release entry (checked October 2026)](https://github.com/koalaman/shellcheck/blob/v0.11.0/CHANGELOG.md#L1). + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/firebase-supabase.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- firebase: Firebase documentation (unknown) +- supabase: Supabase documentation (unknown) +- postgres: PostgreSQL documentation (unknown) +- postgres-view: PostgreSQL invoker-view minimum (15) +- curl: curl (curl-8_14_1) +- shellcheck: ShellCheck (v0.11.0) + +Claims (full source mapping in the JSON block below): + +- public-config (REASONED; firebase, supabase): Firebase API keys/config and Supabase publishable/anon keys are public identifiers, not authorization; hosted endpoints handle TLS. +- firebase-ownership (REASONED; firebase): Require authentication and ownership separately in Firestore, RTDB and Storage; authentication-only rules do not isolate users. +- firebase-starting (REASONED; firebase): Firestore production and RTDB locked modes deny clients; test templates expose data for about a month. Inspect expiry and replace open rules. +- firestore-rules (REASONED; firebase): Rules version 2 recursive wildcards include the user document and descendants; version 1 excludes the parent. Unmatched paths deny and overlapping grants can reopen access. +- rtdb-rules (REASONED; firebase): Owner rules under users.$uid need ancestor grants removed; child rules cannot revoke parent grants or filter parent reads. +- firebase-storage (REASONED; firebase): Storage has separate rules; conflicting default prose/example warrants explicit deny-all followed by deliberate owner-path access. +- firebase-download (REASONED; firebase): Storage download URLs are non-expiring bearer credentials unaffected by tighter Rules; revoke tokens and use authorized getBlob/getBytes direct downloads. +- firebase-rule-deploy (REASONED; firebase): Inspect and test deployed rules for every database/bucket/instance; keep local and deployed definitions synchronized. +- firebase-server (REASONED; firebase): Admin SDK/server credentials bypass client Rules; keep credentials server-side and apply IAM and caller authorization. +- firebase-callable (REASONED; firebase): Callable ID-token validation is optional authentication; reject missing request.auth, use its UID and validate resource access. First-generation handlers use context.auth. +- firebase-callable-appcheck (REASONED; firebase): onCall enforceAppCheck needs firebase-functions 4.0.0 or newer; App Check does not replace caller/resource authorization. +- firebase-http (REASONED; firebase): onRequest does not inherit callable auth; verify end-user tokens with verifyIdToken or restrict IAM invokers, then authorize the intended principal. +- firebase-appcheck (REASONED; firebase): Register compatible clients and inspect metrics, then enforce App Check per product and per callable; registration alone enforces nothing. +- firebase-key-restrictions (REASONED; firebase): Apply one app-restriction type per key plus API restrictions; separate platforms and unrelated billable APIs, using the full Firebase product table. +- firebase-anonymous (REASONED; firebase): Anonymous sign-in is opt-in; disable unused providers because anonymous accounts still satisfy authentication-only Rules. +- firebase-enumeration (REASONED; firebase): Email enumeration protection defaults on only for projects created from September 15, 2023; signup can still return EMAIL_EXISTS. +- firebase-emulators (REASONED; firebase): Emulators default to localhost but missing Firestore/RTDB/Storage Rules configuration means open data; keep listeners, UI, hub, proxies and tunnels private. +- supabase-network (REASONED; supabase): Restrict hosted database/pooler client IPs with Network Restrictions, enforce SSL and protect database-role passwords. +- supabase-grants (REASONED; supabase): Data API access needs grants plus RLS; automatic-grant defaults are transitioning. Table Editor enables RLS, while SQL creation needs explicit enablement. +- supabase-select (REASONED; supabase, postgres): The profiles baseline enables RLS, revokes broad grants and permits authenticated owners SELECT; audit inherited/column grants and existing policies. +- supabase-bypass (REASONED; postgres): With RLS enabled and no policy, ordinary subject roles get no rows; owners normally bypass, as do superusers/BYPASSRLS roles. TRUNCATE is outside RLS. +- supabase-writes (REASONED; supabase, postgres): Grant and policy INSERT/UPDATE/DELETE only when intended; UPDATE checks old/new ownership and needs applicable SELECT when reading columns. +- supabase-schemas (REASONED; supabase): public is exposed by default; restrict exposed schemas and deliberately grant schema/object access. Schema selection does not replace RLS. +- supabase-api-disable (REASONED; supabase): Disable an unused Data API to close generated REST endpoints; this does not disable all other services. +- supabase-keys (REASONED; supabase): Publishable keys select anon or authenticated by session; secret and legacy service_role keys bypass RLS and belong only on trusted servers. +- supabase-key-retire (REASONED; supabase): Replacement keys do not disable legacy keys; migrate consumers then explicitly deactivate legacy keys, or delete compromised secret keys. +- supabase-jwt-retire (REASONED; supabase): Rotation changes the signing key but retains previous trust until revocation; account for verifier caches and propagation. +- supabase-view-owner (REASONED; postgres, supabase): Views use owner privileges/policies by default; disclosure depends on reachable schemas, grants, query and owner privileges, not anon-key possession alone. +- supabase-view-invoker (REASONED; postgres-view, postgres): PostgreSQL 15+ security_invoker views apply caller policies and require underlying privileges. +- supabase-view-legacy (REASONED; postgres): PostgreSQL 14 and earlier require revoking all view privileges from PUBLIC/anon/authenticated or unexposing the schema; updatable views can bypass RLS for writes. +- supabase-functions (REASONED; supabase, postgres): Prefer INVOKER; restrict DEFINER execution, use empty search_path with qualified references and enforce caller authorization including required MFA. +- supabase-function-defaults (REASONED; postgres): New functions grant PUBLIC execution; create privileged functions and restrict grants transactionally. Global/default revocations are creator-specific and do not fix existing functions. +- supabase-http (REASONED; supabase): Audit http/pg_net wrappers and revoke unneeded execute grants; constrain callers/destinations/data. Extension enablement alone does not prove exploitable SSRF. +- supabase-vault (REASONED; supabase): Restrict vault.decrypted_secrets and privileged wrappers and keep Vault unexposed; at-rest encryption cannot prevent authorized plaintext queries. +- supabase-buckets (REASONED; supabase): Private downloads need authorized access or time-limited signed URLs; public buckets bypass retrieval checks but not upload/delete/move/copy controls. +- supabase-storage-policies (REASONED; supabase): Authorize storage.objects separately by bucket and owner path; upload needs INSERT, upsert also SELECT/UPDATE, and deletion needs deliberate permission. +- supabase-realtime-default (REASONED; supabase): Public Realtime channels are allowed by default; disable them project-wide and also set client config.private. +- supabase-realtime-policy (REASONED; supabase): Broadcast/Presence receive/send need separate SELECT/INSERT policies on realtime.messages, constrained by topic ownership and extension. +- supabase-changes (REASONED; supabase): Postgres Changes uses source-table authorization; private-channel policies are not universal and DELETE has separate limitations. +- supabase-signup (REASONED; supabase): Disable signup, anonymous sign-ins and unused providers when unwanted; inspect hosted settings instead of assuming universal defaults. +- supabase-cli-auth (REASONED; supabase): CLI enable_signup defaults true and enable_anonymous_sign_ins false; local config edits do not prove hosted settings changed. +- supabase-mfa (REASONED; supabase, postgres): TOTP is available on every plan at writing; enrollment alone does not constrain data. Require aal2 using a restrictive policy alongside permissive ownership policies. +- supabase-edge-jwt (REASONED; supabase): Per-function verify_jwt defaults true; --no-verify-jwt disables platform checking and requires handler authentication before protected work. +- supabase-edge-auth (REASONED; supabase): Platform JWT success is not resource authorization and API-key compatibility can pass it; separately authorize users or verify webhook signatures. +- supabase-edge-secrets (REASONED; supabase): Edge environments contain privileged credentials including SUPABASE_SERVICE_ROLE_KEY; protect managed/local secrets, responses, logs and client bundles. +- verify-firestore (REASONED; firebase): Compare unsigned/A/B reads and writes to user documents and descendants, with a successful owner control; test parent documents to distinguish recursive-rule versions. +- verify-rtdb (REASONED; firebase): Compare unsigned/A/B reads/writes to seeded RTDB owner paths; ancestor grants expose them while ownership rules deny unauthorized callers. +- verify-firebase-storage (REASONED; firebase): Compare unsigned/A/B object reads and uploads; ownership rules must deny unrelated callers while owner operations succeed. +- verify-download-token (REASONED; firebase): Same sessionless download URL works after Rules tightening but must lose bytes after token revocation; owner getBytes remains successful and network/CORS failures are inconclusive. +- verify-tables (REASONED; supabase, postgres, curl): Pair unsigned/A/B profiles GETs against seeded rows, using ordinary credentials and required MFA; test intended writes and persisted effects separately. +- verify-views (REASONED; postgres): Compare view reads with table boundaries; owner-privileged exposure must close under invoker/revoked access while intended reads or deliberate removal are confirmed. +- verify-rpc (REASONED; supabase): Test every API function overload with valid harmless arguments as unsigned/A/B; unauthorized data or operations must be denied while the intended caller succeeds. +- verify-http (REASONED; supabase): Probe HTTP wrappers with a controlled collector and marker; unauthorized fixed calls must emit no request. Commit pg_net fixture transactions before expecting delivery. +- verify-vault (REASONED; supabase, postgres): Use a disposable Vault canary to compare exposed/fixed wrappers; inspect function and table privileges as supporting evidence. +- verify-storage (REASONED; supabase): Compare public URL and A/B private downloads plus intended uploads/upserts; unauthorized callers must receive no bytes or cross-owner writes. +- verify-realtime (REASONED; supabase): Compare A/B join, Broadcast and Presence delivery; public-disabled clients lacking private=true must fail, and connection alone is insufficient. +- verify-changes (REASONED; supabase): Observe A/B INSERT/UPDATE subscription events against seeded rows; fixed table authorization withholds unrelated events. Evaluate DELETE separately. +- verify-edge (REASONED; supabase): Compare no/invalid Bearer without apikey, authorized owners and valid webhook signatures; distinguish platform 401 from handler authorization and inspect effects/secrets. +- verify-firebase-functions (REASONED; firebase): Compare callable requests with valid App Check but missing user auth and A/B operations; HTTP endpoints need separate invalid/valid user or IAM comparisons. +- verify-appcheck (REASONED; firebase): Repeat owner requests with/without App Check per product/callable; enforcement must deny missing attestation while compatible clients work. +- verify-api-restrictions (REASONED; firebase): Test intended versus excluded apps/referrers after key restrictions, retaining successful intended product traffic. +- verify-emulators (REASONED; firebase): Enumerate hub /emulators and sockets, compare missing versus loaded Rules and second-host connectivity, and retain local owner controls. +- verify-firebase-accounts (REASONED; firebase): Sign out before each Anonymous signup test and check isNewUser; disabled providers must reject creation. Enumeration tests compare sign-in errors, not EMAIL_EXISTS signup behavior. +- verify-supabase-accounts (REASONED; supabase): Fresh signup and anonymous creation must fail under invite-only/anonymous-disabled settings while an invited account still signs in. +- verify-mfa (REASONED; supabase): Owner profiles GET with aal1 must yield no protected rows under the restrictive policy, while fresh aal2 returns the seeded owner row. +- verify-key-retirement (REASONED; supabase): Compare old/new API keys after explicit deactivation and old/new JWTs after revocation, accounting for propagation and caches. +- verify-api-removal (REASONED; supabase): Previously successful profiles requests must stop returning fixtures after schema removal or Data API disabling; retained schemas need authorized controls. +- verify-bundle (REASONED; supabase): Filename-only service_role/eyJ/sb_secret_ scanning yields review candidates, not proof of leaks; public anon JWTs are legitimate. Use a marker control and inspect encoded/split secrets separately. +- local-guards (DEMONSTRATED; supabase, shellcheck): Historical local guard tests rejected placeholders and malformed set lines; this guide's assessment is shell behavior only, not live authorization. The recorded ShellCheck 0.11.0 has a release citation, not host-binary evidence; bash and other tool/parser versions remain unrecorded. +- local-syntax (DEMONSTRATED; firebase, supabase, shellcheck): Recorded ShellCheck 0.11.0/bash and JSON/TOML/JavaScript parsing passed; this guide's assessment is that these checks do not compile Firebase Rules, execute SQL or demonstrate services. Only the ShellCheck release is cited; bash and other tool/parser versions remain unrecorded. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "6690687db9e1d66f3e67a898fc0b030022e55be24166bc29b8c35554e8081891", + "components": { + "firebase": { + "name": "Firebase documentation", + "basis": "unknown", + "sources": { + "s100c2cdbdd59": "https://firebase.google.com/docs/rules", + "s53bfe8b21868": "https://firebase.google.com/docs/rules/basics", + "s622d3bb0665b": "https://firebase.google.com/docs/firestore/quickstart", + "sb2c933c841ef": "https://firebase.google.com/docs/database/web/start", + "s75f018e0fac7": "https://firebase.google.com/support/releases", + "s7bd11c22c7ce": "https://firebase.google.com/docs/firestore/security/rules-structure", + "sa6b1c4933e6a": "https://firebase.google.com/docs/database/security/core-syntax", + "s0a7f54463abd": "https://firebase.google.com/docs/storage/security/core-syntax", + "sd6071548f6fe": "https://firebase.google.com/docs/firestore/security/get-started", + "s14872143a9e8": "https://firebase.google.com/docs/storage/security/get-started", + "sc8f214d059d2": "https://firebase.google.com/docs/functions/callable", + "s79da8f65f90a": "https://firebase.google.com/docs/functions/callable-reference", + "s9a86df9623a6": "https://firebase.google.com/docs/functions/1st-gen/callable-1st", + "s49f78d054708": "https://firebase.google.com/docs/auth/admin/verify-id-tokens", + "s1bb711c9afeb": "https://firebase.google.com/docs/functions/http-events", + "sc9f86ea271f6": "https://firebase.google.com/docs/reference/functions/2nd-gen/node/firebase-functions.https.httpsoptions", + "sb0ff8f1726f7": "https://firebase.google.com/docs/app-check/cloud-functions", + "sa71e958c99df": "https://firebase.google.com/docs/app-check/web/recaptcha-enterprise-provider", + "sba7129c83405": "https://firebase.google.com/docs/app-check/enable-enforcement", + "safeed4cd2b67": "https://firebase.google.com/docs/projects/api-keys", + "sc78d5eb59b65": "https://docs.cloud.google.com/api-keys/docs/add-restrictions-api-keys", + "sb0d26775f3b4": "https://firebase.google.com/docs/web/learn-more", + "s77e2cbd8a29a": "https://firebase.google.com/docs/auth/web/anonymous-auth", + "s06808e4b6965": "https://docs.cloud.google.com/identity-platform/docs/admin/email-enumeration-protection", + "sd8a1bda29f5c": "https://firebase.google.com/docs/emulator-suite/install_and_configure", + "s69d9721c7f29": "https://firebase.google.com/docs/emulator-suite/use_hosting", + "s8c90f9f6c7fa": "https://firebase.google.com/docs/reference/js/auth", + "s8c8336ec252e": "https://firebase.google.com/docs/reference/js/auth.additionaluserinfo", + "s142e56472093": "https://firebase.google.com/docs/auth/admin/manage-sessions", + "s9b7e57b472c7": "https://firebase.google.com/docs/storage/admin/start", + "sa0584b797732": "https://firebase.google.com/docs/reference/kotlin/com/google/firebase/storage/StorageReference", + "sfd1e7412abf6": "https://firebase.google.com/docs/storage/web/download-files" + } + }, + "supabase": { + "name": "Supabase documentation", + "basis": "unknown", + "sources": { + "s73d939b4644e": "https://supabase.com/docs/guides/api/api-keys", + "s46cff2590323": "https://supabase.com/docs/guides/database/postgres/row-level-security", + "s741468ff35a9": "https://supabase.com/docs/guides/auth/auth-mfa", + "s6d5cf2d4dbf3": "https://supabase.com/docs/guides/api/securing-your-api", + "s20ef046ac646": "https://supabase.com/docs/guides/api/using-custom-schemas", + "sa946354db383": "https://supabase.com/docs/guides/getting-started/migrating-to-new-api-keys", + "sfc2f9ae0232b": "https://supabase.com/docs/guides/auth/signing-keys", + "s8405a9fb4b74": "https://supabase.com/docs/guides/database/functions", + "s13da01bbbba5": "https://supabase.com/docs/guides/database/extensions/http", + "s76740bffcb32": "https://supabase.com/docs/guides/database/extensions/pg_net", + "s47bb8fa3e14e": "https://supabase.com/docs/guides/database/vault", + "s1567fb838f20": "https://supabase.com/docs/guides/storage/buckets/fundamentals", + "sdba7d7b6dcf8": "https://supabase.com/docs/guides/storage/security/access-control", + "s9e1484f46eff": "https://supabase.com/docs/guides/storage/schema/helper-functions", + "sc29febbb230e": "https://supabase.com/docs/guides/storage/serving/downloads", + "see2d3be99ebd": "https://supabase.com/docs/guides/storage/uploads/standard-uploads", + "s5674b1057e14": "https://supabase.com/docs/guides/realtime/settings", + "s84efad0ba39d": "https://supabase.com/docs/guides/realtime/authorization", + "s694eab64f2ec": "https://supabase.com/docs/guides/realtime/postgres-changes", + "sc797f0b63472": "https://supabase.com/docs/guides/auth/general-configuration", + "s3a5e39fe7b4d": "https://supabase.com/docs/guides/local-development/cli/config", + "sa2dd4637969e": "https://supabase.com/docs/guides/auth/passwords", + "sad79d4d1b6ee": "https://supabase.com/docs/guides/auth/auth-anonymous", + "s182755725d74": "https://supabase.com/docs/guides/auth/auth-mfa/totp", + "s92b405dc3c7e": "https://supabase.com/docs/reference/javascript/select", + "s087c8cf42ab0": "https://supabase.com/docs/reference/javascript/rpc", + "sf3246532dd18": "https://supabase.com/docs/guides/functions", + "s7dfbb47b5d20": "https://supabase.com/docs/reference/cli/supabase-functions-deploy", + "sf7d438b8c080": "https://supabase.com/docs/guides/functions/auth", + "se52bbd097033": "https://supabase.com/docs/guides/functions/auth-headers", + "sb3c4c0993e14": "https://supabase.com/docs/guides/functions/secrets", + "sb787b5d63633": "https://supabase.com/docs/guides/platform/network-restrictions", + "s83e15676e1ff": "https://supabase.com/docs/guides/platform/ssl-enforcement", + "sf69cafb4bc6c": "https://supabase.com/docs/guides/database/postgres/roles" + } + }, + "postgres": { + "name": "PostgreSQL documentation", + "basis": "unknown", + "sources": { + "s13c54ab9cc6b": "https://www.postgresql.org/docs/current/sql-createview.html", + "saa99ef28f60f": "https://www.postgresql.org/docs/current/sql-alterview.html", + "sb6224f4ac866": "https://www.postgresql.org/docs/current/sql-revoke.html", + "se9846041ce1f": "https://www.postgresql.org/docs/current/ddl-rowsecurity.html", + "s8e65a2f3f2bb": "https://www.postgresql.org/docs/current/sql-createpolicy.html", + "sec39c317b06f": "https://www.postgresql.org/docs/current/sql-createfunction.html", + "s0fe66d71aa10": "https://www.postgresql.org/docs/current/sql-alterfunction.html", + "s04e81888353f": "https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html", + "sc39eceeac711": "https://www.postgresql.org/docs/current/functions-info.html" + } + }, + "postgres-view": { + "name": "PostgreSQL invoker-view minimum", + "basis": "15", + "sources": { + "s14443e063ef1": "https://www.postgresql.org/docs/release/15.0/" + } + }, + "curl": { + "name": "curl", + "basis": "curl-8_14_1", + "sources": { + "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", + "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md" + } + }, + "shellcheck": { + "name": "ShellCheck", + "basis": "v0.11.0", + "sources": { + "sf994764bf270": "https://github.com/koalaman/shellcheck/blob/v0.11.0/CHANGELOG.md#L1" + } + } + }, + "claims": { + "public-config": {"text": "Firebase API keys/config and Supabase publishable/anon keys are public identifiers, not authorization; hosted endpoints handle TLS.", "components": ["firebase", "supabase"], "sources": ["firebase:safeed4cd2b67", "firebase:sb0d26775f3b4", "supabase:s73d939b4644e"], "status": "REASONED"}, + "firebase-ownership": {"text": "Require authentication and ownership separately in Firestore, RTDB and Storage; authentication-only rules do not isolate users.", "components": ["firebase"], "sources": ["firebase:s100c2cdbdd59", "firebase:s53bfe8b21868"], "status": "REASONED"}, + "firebase-starting": {"text": "Firestore production and RTDB locked modes deny clients; test templates expose data for about a month. Inspect expiry and replace open rules.", "components": ["firebase"], "sources": ["firebase:s622d3bb0665b", "firebase:sb2c933c841ef", "firebase:s75f018e0fac7"], "status": "REASONED"}, + "firestore-rules": {"text": "Rules version 2 recursive wildcards include the user document and descendants; version 1 excludes the parent. Unmatched paths deny and overlapping grants can reopen access.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, + "rtdb-rules": {"text": "Owner rules under users.$uid need ancestor grants removed; child rules cannot revoke parent grants or filter parent reads.", "components": ["firebase"], "sources": ["firebase:sa6b1c4933e6a"], "status": "REASONED"}, + "firebase-storage": {"text": "Storage has separate rules; conflicting default prose/example warrants explicit deny-all followed by deliberate owner-path access.", "components": ["firebase"], "sources": ["firebase:s53bfe8b21868", "firebase:s0a7f54463abd"], "status": "REASONED"}, + "firebase-download": {"text": "Storage download URLs are non-expiring bearer credentials unaffected by tighter Rules; revoke tokens and use authorized getBlob/getBytes direct downloads.", "components": ["firebase"], "sources": ["firebase:s9b7e57b472c7", "firebase:sa0584b797732", "firebase:sfd1e7412abf6"], "status": "REASONED"}, + "firebase-rule-deploy": {"text": "Inspect and test deployed rules for every database/bucket/instance; keep local and deployed definitions synchronized.", "components": ["firebase"], "sources": ["firebase:sd6071548f6fe", "firebase:s14872143a9e8"], "status": "REASONED"}, + "firebase-server": {"text": "Admin SDK/server credentials bypass client Rules; keep credentials server-side and apply IAM and caller authorization.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, + "firebase-callable": {"text": "Callable ID-token validation is optional authentication; reject missing request.auth, use its UID and validate resource access. First-generation handlers use context.auth.", "components": ["firebase"], "sources": ["firebase:s79da8f65f90a", "firebase:sc8f214d059d2", "firebase:s9a86df9623a6"], "status": "REASONED"}, + "firebase-callable-appcheck": {"text": "onCall enforceAppCheck needs firebase-functions 4.0.0 or newer; App Check does not replace caller/resource authorization.", "components": ["firebase"], "sources": ["firebase:sb0ff8f1726f7"], "status": "REASONED"}, + "firebase-http": {"text": "onRequest does not inherit callable auth; verify end-user tokens with verifyIdToken or restrict IAM invokers, then authorize the intended principal.", "components": ["firebase"], "sources": ["firebase:s49f78d054708", "firebase:s1bb711c9afeb", "firebase:sc9f86ea271f6"], "status": "REASONED"}, + "firebase-appcheck": {"text": "Register compatible clients and inspect metrics, then enforce App Check per product and per callable; registration alone enforces nothing.", "components": ["firebase"], "sources": ["firebase:sa71e958c99df", "firebase:sba7129c83405", "firebase:sb0ff8f1726f7"], "status": "REASONED"}, + "firebase-key-restrictions": {"text": "Apply one app-restriction type per key plus API restrictions; separate platforms and unrelated billable APIs, using the full Firebase product table.", "components": ["firebase"], "sources": ["firebase:sc78d5eb59b65", "firebase:safeed4cd2b67"], "status": "REASONED"}, + "firebase-anonymous": {"text": "Anonymous sign-in is opt-in; disable unused providers because anonymous accounts still satisfy authentication-only Rules.", "components": ["firebase"], "sources": ["firebase:s77e2cbd8a29a"], "status": "REASONED"}, + "firebase-enumeration": {"text": "Email enumeration protection defaults on only for projects created from September 15, 2023; signup can still return EMAIL_EXISTS.", "components": ["firebase"], "sources": ["firebase:s06808e4b6965"], "status": "REASONED"}, + "firebase-emulators": {"text": "Emulators default to localhost but missing Firestore/RTDB/Storage Rules configuration means open data; keep listeners, UI, hub, proxies and tunnels private.", "components": ["firebase"], "sources": ["firebase:sd8a1bda29f5c", "firebase:s69d9721c7f29"], "status": "REASONED"}, + "supabase-network": {"text": "Restrict hosted database/pooler client IPs with Network Restrictions, enforce SSL and protect database-role passwords.", "components": ["supabase"], "sources": ["supabase:sb787b5d63633", "supabase:s83e15676e1ff", "supabase:sf69cafb4bc6c"], "status": "REASONED"}, + "supabase-grants": {"text": "Data API access needs grants plus RLS; automatic-grant defaults are transitioning. Table Editor enables RLS, while SQL creation needs explicit enablement.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3"], "status": "REASONED"}, + "supabase-select": {"text": "The profiles baseline enables RLS, revokes broad grants and permits authenticated owners SELECT; audit inherited/column grants and existing policies.", "components": ["supabase", "postgres"], "sources": ["supabase:s46cff2590323", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, + "supabase-bypass": {"text": "With RLS enabled and no policy, ordinary subject roles get no rows; owners normally bypass, as do superusers/BYPASSRLS roles. TRUNCATE is outside RLS.", "components": ["postgres"], "sources": ["postgres:se9846041ce1f"], "status": "REASONED"}, + "supabase-writes": {"text": "Grant and policy INSERT/UPDATE/DELETE only when intended; UPDATE checks old/new ownership and needs applicable SELECT when reading columns.", "components": ["supabase", "postgres"], "sources": ["supabase:s46cff2590323", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, + "supabase-schemas": {"text": "public is exposed by default; restrict exposed schemas and deliberately grant schema/object access. Schema selection does not replace RLS.", "components": ["supabase"], "sources": ["supabase:s20ef046ac646"], "status": "REASONED"}, + "supabase-api-disable": {"text": "Disable an unused Data API to close generated REST endpoints; this does not disable all other services.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3"], "status": "REASONED"}, + "supabase-keys": {"text": "Publishable keys select anon or authenticated by session; secret and legacy service_role keys bypass RLS and belong only on trusted servers.", "components": ["supabase"], "sources": ["supabase:s73d939b4644e"], "status": "REASONED"}, + "supabase-key-retire": {"text": "Replacement keys do not disable legacy keys; migrate consumers then explicitly deactivate legacy keys, or delete compromised secret keys.", "components": ["supabase"], "sources": ["supabase:sa946354db383"], "status": "REASONED"}, + "supabase-jwt-retire": {"text": "Rotation changes the signing key but retains previous trust until revocation; account for verifier caches and propagation.", "components": ["supabase"], "sources": ["supabase:sfc2f9ae0232b"], "status": "REASONED"}, + "supabase-view-owner": {"text": "Views use owner privileges/policies by default; disclosure depends on reachable schemas, grants, query and owner privileges, not anon-key possession alone.", "components": ["postgres", "supabase"], "sources": ["postgres:s13c54ab9cc6b", "supabase:s46cff2590323"], "status": "REASONED"}, + "supabase-view-invoker": {"text": "PostgreSQL 15+ security_invoker views apply caller policies and require underlying privileges.", "components": ["postgres-view", "postgres"], "sources": ["postgres-view:s14443e063ef1", "postgres:saa99ef28f60f", "postgres:s13c54ab9cc6b"], "status": "REASONED"}, + "supabase-view-legacy": {"text": "PostgreSQL 14 and earlier require revoking all view privileges from PUBLIC/anon/authenticated or unexposing the schema; updatable views can bypass RLS for writes.", "components": ["postgres"], "sources": ["postgres:s13c54ab9cc6b", "postgres:sb6224f4ac866", "postgres:sc39eceeac711"], "status": "REASONED"}, + "supabase-functions": {"text": "Prefer INVOKER; restrict DEFINER execution, use empty search_path with qualified references and enforce caller authorization including required MFA.", "components": ["supabase", "postgres"], "sources": ["supabase:s8405a9fb4b74", "postgres:sec39c317b06f", "postgres:s0fe66d71aa10"], "status": "REASONED"}, + "supabase-function-defaults": {"text": "New functions grant PUBLIC execution; create privileged functions and restrict grants transactionally. Global/default revocations are creator-specific and do not fix existing functions.", "components": ["postgres"], "sources": ["postgres:sec39c317b06f", "postgres:s04e81888353f"], "status": "REASONED"}, + "supabase-http": {"text": "Audit http/pg_net wrappers and revoke unneeded execute grants; constrain callers/destinations/data. Extension enablement alone does not prove exploitable SSRF.", "components": ["supabase"], "sources": ["supabase:s13da01bbbba5", "supabase:s76740bffcb32"], "status": "REASONED"}, + "supabase-vault": {"text": "Restrict vault.decrypted_secrets and privileged wrappers and keep Vault unexposed; at-rest encryption cannot prevent authorized plaintext queries.", "components": ["supabase"], "sources": ["supabase:s47bb8fa3e14e"], "status": "REASONED"}, + "supabase-buckets": {"text": "Private downloads need authorized access or time-limited signed URLs; public buckets bypass retrieval checks but not upload/delete/move/copy controls.", "components": ["supabase"], "sources": ["supabase:s1567fb838f20"], "status": "REASONED"}, + "supabase-storage-policies": {"text": "Authorize storage.objects separately by bucket and owner path; upload needs INSERT, upsert also SELECT/UPDATE, and deletion needs deliberate permission.", "components": ["supabase"], "sources": ["supabase:sdba7d7b6dcf8", "supabase:s9e1484f46eff", "supabase:see2d3be99ebd"], "status": "REASONED"}, + "supabase-realtime-default": {"text": "Public Realtime channels are allowed by default; disable them project-wide and also set client config.private.", "components": ["supabase"], "sources": ["supabase:s5674b1057e14"], "status": "REASONED"}, + "supabase-realtime-policy": {"text": "Broadcast/Presence receive/send need separate SELECT/INSERT policies on realtime.messages, constrained by topic ownership and extension.", "components": ["supabase"], "sources": ["supabase:s84efad0ba39d"], "status": "REASONED"}, + "supabase-changes": {"text": "Postgres Changes uses source-table authorization; private-channel policies are not universal and DELETE has separate limitations.", "components": ["supabase"], "sources": ["supabase:s694eab64f2ec"], "status": "REASONED"}, + "supabase-signup": {"text": "Disable signup, anonymous sign-ins and unused providers when unwanted; inspect hosted settings instead of assuming universal defaults.", "components": ["supabase"], "sources": ["supabase:sc797f0b63472"], "status": "REASONED"}, + "supabase-cli-auth": {"text": "CLI enable_signup defaults true and enable_anonymous_sign_ins false; local config edits do not prove hosted settings changed.", "components": ["supabase"], "sources": ["supabase:s3a5e39fe7b4d"], "status": "REASONED"}, + "supabase-mfa": {"text": "TOTP is available on every plan at writing; enrollment alone does not constrain data. Require aal2 using a restrictive policy alongside permissive ownership policies.", "components": ["supabase", "postgres"], "sources": ["supabase:s182755725d74", "supabase:s741468ff35a9", "postgres:s8e65a2f3f2bb"], "status": "REASONED"}, + "supabase-edge-jwt": {"text": "Per-function verify_jwt defaults true; --no-verify-jwt disables platform checking and requires handler authentication before protected work.", "components": ["supabase"], "sources": ["supabase:s7dfbb47b5d20", "supabase:sf7d438b8c080", "supabase:se52bbd097033"], "status": "REASONED"}, + "supabase-edge-auth": {"text": "Platform JWT success is not resource authorization and API-key compatibility can pass it; separately authorize users or verify webhook signatures.", "components": ["supabase"], "sources": ["supabase:sf7d438b8c080", "supabase:se52bbd097033"], "status": "REASONED"}, + "supabase-edge-secrets": {"text": "Edge environments contain privileged credentials including SUPABASE_SERVICE_ROLE_KEY; protect managed/local secrets, responses, logs and client bundles.", "components": ["supabase"], "sources": ["supabase:sb3c4c0993e14"], "status": "REASONED"}, + "verify-firestore": {"text": "Compare unsigned/A/B reads and writes to user documents and descendants, with a successful owner control; test parent documents to distinguish recursive-rule versions.", "components": ["firebase"], "sources": ["firebase:s7bd11c22c7ce"], "status": "REASONED"}, + "verify-rtdb": {"text": "Compare unsigned/A/B reads/writes to seeded RTDB owner paths; ancestor grants expose them while ownership rules deny unauthorized callers.", "components": ["firebase"], "sources": ["firebase:sa6b1c4933e6a"], "status": "REASONED"}, + "verify-firebase-storage": {"text": "Compare unsigned/A/B object reads and uploads; ownership rules must deny unrelated callers while owner operations succeed.", "components": ["firebase"], "sources": ["firebase:s0a7f54463abd"], "status": "REASONED"}, + "verify-download-token": {"text": "Same sessionless download URL works after Rules tightening but must lose bytes after token revocation; owner getBytes remains successful and network/CORS failures are inconclusive.", "components": ["firebase"], "sources": ["firebase:s9b7e57b472c7", "firebase:sa0584b797732", "firebase:sfd1e7412abf6"], "status": "REASONED"}, + "verify-tables": {"text": "Pair unsigned/A/B profiles GETs against seeded rows, using ordinary credentials and required MFA; test intended writes and persisted effects separately.", "components": ["supabase", "postgres", "curl"], "sources": ["supabase:s92b405dc3c7e", "postgres:se9846041ce1f", "supabase:s741468ff35a9", "curl:s5e1383feef7d", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, + "verify-views": {"text": "Compare view reads with table boundaries; owner-privileged exposure must close under invoker/revoked access while intended reads or deliberate removal are confirmed.", "components": ["postgres"], "sources": ["postgres:s13c54ab9cc6b"], "status": "REASONED"}, + "verify-rpc": {"text": "Test every API function overload with valid harmless arguments as unsigned/A/B; unauthorized data or operations must be denied while the intended caller succeeds.", "components": ["supabase"], "sources": ["supabase:s087c8cf42ab0", "supabase:s8405a9fb4b74"], "status": "REASONED"}, + "verify-http": {"text": "Probe HTTP wrappers with a controlled collector and marker; unauthorized fixed calls must emit no request. Commit pg_net fixture transactions before expecting delivery.", "components": ["supabase"], "sources": ["supabase:s76740bffcb32"], "status": "REASONED"}, + "verify-vault": {"text": "Use a disposable Vault canary to compare exposed/fixed wrappers; inspect function and table privileges as supporting evidence.", "components": ["supabase", "postgres"], "sources": ["supabase:s47bb8fa3e14e", "postgres:sc39eceeac711"], "status": "REASONED"}, + "verify-storage": {"text": "Compare public URL and A/B private downloads plus intended uploads/upserts; unauthorized callers must receive no bytes or cross-owner writes.", "components": ["supabase"], "sources": ["supabase:sc29febbb230e", "supabase:see2d3be99ebd", "supabase:s1567fb838f20"], "status": "REASONED"}, + "verify-realtime": {"text": "Compare A/B join, Broadcast and Presence delivery; public-disabled clients lacking private=true must fail, and connection alone is insufficient.", "components": ["supabase"], "sources": ["supabase:s5674b1057e14", "supabase:s84efad0ba39d"], "status": "REASONED"}, + "verify-changes": {"text": "Observe A/B INSERT/UPDATE subscription events against seeded rows; fixed table authorization withholds unrelated events. Evaluate DELETE separately.", "components": ["supabase"], "sources": ["supabase:s694eab64f2ec"], "status": "REASONED"}, + "verify-edge": {"text": "Compare no/invalid Bearer without apikey, authorized owners and valid webhook signatures; distinguish platform 401 from handler authorization and inspect effects/secrets.", "components": ["supabase"], "sources": ["supabase:se52bbd097033", "supabase:sf7d438b8c080", "supabase:sb3c4c0993e14"], "status": "REASONED"}, + "verify-firebase-functions": {"text": "Compare callable requests with valid App Check but missing user auth and A/B operations; HTTP endpoints need separate invalid/valid user or IAM comparisons.", "components": ["firebase"], "sources": ["firebase:s79da8f65f90a", "firebase:s49f78d054708"], "status": "REASONED"}, + "verify-appcheck": {"text": "Repeat owner requests with/without App Check per product/callable; enforcement must deny missing attestation while compatible clients work.", "components": ["firebase"], "sources": ["firebase:sba7129c83405", "firebase:sb0ff8f1726f7"], "status": "REASONED"}, + "verify-api-restrictions": {"text": "Test intended versus excluded apps/referrers after key restrictions, retaining successful intended product traffic.", "components": ["firebase"], "sources": ["firebase:safeed4cd2b67", "firebase:sc78d5eb59b65"], "status": "REASONED"}, + "verify-emulators": {"text": "Enumerate hub /emulators and sockets, compare missing versus loaded Rules and second-host connectivity, and retain local owner controls.", "components": ["firebase"], "sources": ["firebase:sd8a1bda29f5c", "firebase:s69d9721c7f29"], "status": "REASONED"}, + "verify-firebase-accounts": {"text": "Sign out before each Anonymous signup test and check isNewUser; disabled providers must reject creation. Enumeration tests compare sign-in errors, not EMAIL_EXISTS signup behavior.", "components": ["firebase"], "sources": ["firebase:s8c90f9f6c7fa", "firebase:s8c8336ec252e", "firebase:s142e56472093", "firebase:s77e2cbd8a29a", "firebase:s06808e4b6965"], "status": "REASONED"}, + "verify-supabase-accounts": {"text": "Fresh signup and anonymous creation must fail under invite-only/anonymous-disabled settings while an invited account still signs in.", "components": ["supabase"], "sources": ["supabase:sc797f0b63472", "supabase:sa2dd4637969e", "supabase:sad79d4d1b6ee"], "status": "REASONED"}, + "verify-mfa": {"text": "Owner profiles GET with aal1 must yield no protected rows under the restrictive policy, while fresh aal2 returns the seeded owner row.", "components": ["supabase"], "sources": ["supabase:s741468ff35a9"], "status": "REASONED", "verify": [1]}, + "verify-key-retirement": {"text": "Compare old/new API keys after explicit deactivation and old/new JWTs after revocation, accounting for propagation and caches.", "components": ["supabase"], "sources": ["supabase:sa946354db383", "supabase:sfc2f9ae0232b"], "status": "REASONED"}, + "verify-api-removal": {"text": "Previously successful profiles requests must stop returning fixtures after schema removal or Data API disabling; retained schemas need authorized controls.", "components": ["supabase"], "sources": ["supabase:s6d5cf2d4dbf3", "supabase:s20ef046ac646"], "status": "REASONED"}, + "verify-bundle": {"text": "Filename-only service_role/eyJ/sb_secret_ scanning yields review candidates, not proof of leaks; public anon JWTs are legitimate. Use a marker control and inspect encoded/split secrets separately.", "components": ["supabase"], "sources": ["supabase:s73d939b4644e"], "status": "REASONED", "verify": [2]}, + "local-guards": {"text": "Historical local guard tests rejected placeholders and malformed set lines; this guide's assessment is shell behavior only, not live authorization. The recorded ShellCheck 0.11.0 has a release citation, not host-binary evidence; bash and other tool/parser versions remain unrecorded.", "components": ["supabase", "shellcheck"], "sources": ["supabase:s92b405dc3c7e", "supabase:s73d939b4644e", "shellcheck:sf994764bf270"], "status": "DEMONSTRATED", "evidence": "Local guard tests refused embedded `REPLACE_WITH_` placeholders, `example.com`, angle brackets, empty values, and omitted or shortened `set --` lines."}, + "local-syntax": {"text": "Recorded ShellCheck 0.11.0/bash and JSON/TOML/JavaScript parsing passed; this guide's assessment is that these checks do not compile Firebase Rules, execute SQL or demonstrate services. Only the ShellCheck release is cited; bash and other tool/parser versions remain unrecorded.", "components": ["firebase", "supabase", "shellcheck"], "sources": ["firebase:sa6b1c4933e6a", "firebase:sd8a1bda29f5c", "supabase:s3a5e39fe7b4d", "firebase:sc8f214d059d2", "shellcheck:sf994764bf270"], "status": "DEMONSTRATED", "evidence": "The RTDB JSON, `firebase.json` fragment, Auth TOML, and callable JavaScript passed local parsing checks."} + } +} +```` + + ====================================================================== ==> supabase-self-hosted.md ====================================================================== @@ -44917,7 +48346,7 @@ done # WebSockets, server-sent events, and webhooks: authenticating the non-page endpoints -Written against WebSocket protocol RFC 6455, stripe-node v18.5.0, stripe-python v12.5.0, Traefik timeout reference v3.5, Caddy source v2.11.4, nginx source release-1.26.3, HAProxy 3.0, and HAProxy source v3.0.0, plus 1 more recorded bases; 7 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 36 claims, all reasoned from cited sources. +Written against WebSocket protocol RFC 6455, stripe-node v18.5.0, stripe-python v12.5.0, Traefik timeout reference v3.5, Caddy source v2.11.4, nginx source release-1.26.3, HAProxy 3.0, and HAProxy source v3.0.0, plus 1 more recorded basis; 7 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 36 claims, all reasoned from cited sources. A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. @@ -46753,63 +50182,9 @@ give the server's public address. A "connected" on a port you did not mean to ex # Nomad and Consul: the scheduler and service-mesh APIs - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-09-26; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- nomad: Nomad Community Edition (2.0.7) -- consul: Consul Community Edition (2.0.4) - -Claims (full source mapping in the front matter): +Written against Nomad Community Edition 2.0.7 and Consul Community Edition 2.0.4. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 42 claims: 13 demonstrated by a check that ran and 29 reasoned from cited sources. -- nomad-bind (REASONED; nomad): Nomad defaults bind_addr=0.0.0.0 and all three listeners fall back to it; setting only addresses.http leaves RPC and gossip on bind_addr. -- nomad-http (REASONED; nomad): Nomad HTTP API/UI uses 4646; UI defaults enabled at /ui/. -- nomad-rpc (REASONED; nomad): Nomad RPC uses 4647; keep it on private or management addresses. -- nomad-gossip (REASONED; nomad): Nomad Serf uses 4648 TCP/UDP; gossip is unencrypted until encrypt is configured. -- nomad-dev (REASONED; nomad): Linux -dev binds 127.0.0.1 except -dev-connect, which binds 0.0.0.0. Wildcard behavior was not observed. -- nomad-acl-default (REASONED; nomad): ACLs default disabled; enable acl.enabled on every agent before exposing the API. -- nomad-debug (REASONED; nomad): Debug endpoints still require enable_debug with ACLs off; -dev enables it. -- nomad-exec (REASONED; nomad): On a root Linux client, exec uses a chroot and task user, default nobody, with root refused by the default exec denylist; no client job was run. -- nomad-raw-exec (REASONED; nomad): raw_exec has no isolation and defaults off, but -dev enables it. -- nomad-remote-exec (REASONED; nomad): Remote task execution defaults enabled through disable_remote_exec=false. -- nomad-tls (REASONED; nomad): HTTP/RPC TLS defaults off; enable both tls.http and tls.rpc. -- nomad-bootstrap-reset (REASONED; nomad): Unauthenticated PUT /v1/acl/bootstrap claims the first management token after ACL enablement; repeat is blocked until the operator reset file in data_dir/server. -- consul-client-bind (REASONED; consul): Consul client_addr defaults 127.0.0.1; widening it or addresses.http makes the API reachable. -- consul-http (REASONED; consul): Consul HTTP API is 8500; UI defaults off and -dev enables /ui/ on that port. -- consul-dns (REASONED; consul): Consul DNS uses 8600 TCP/UDP on client_addr. -- consul-rpc (REASONED; consul): Consul server RPC uses 8300 on bind_addr, default 0.0.0.0. -- consul-lan (REASONED; consul): Consul LAN Serf uses 8301 TCP/UDP on bind_addr. -- consul-wan (REASONED; consul): Consul WAN Serf uses 8302 TCP/UDP on bind_addr. -- consul-https (REASONED; consul): Consul HTTPS defaults disabled. -- consul-grpc (REASONED; consul): Plaintext gRPC defaults disabled; servers open TLS gRPC on 8503 on client_addr. -- consul-acl-default (REASONED; consul): Consul ACLs default off and disabled ACL resolution grants management rights; with ACLs enabled default_policy still defaults allow. -- consul-bootstrap-reset (REASONED; consul): Bootstrap is unauthenticated and one-time until an operator writes acl-bootstrap-reset; bootstrap privately before exposing the API. -- consul-script (REASONED; consul): enable_script_checks defaults off; enabling without ACLs or allow_write_http_from only warns. Prefer enable_local_script_checks if needed. -- consul-remote-exec (REASONED; consul): consul exec defaults disabled through disable_remote_exec=true. -- consul-tls-gossip (REASONED; consul): TLS verification defaults off and gossip is unencrypted without encrypt. -- verify-inventory (REASONED; nomad, consul): Inventory TCP/UDP on private or management addresses; wildcard exposed binds and actual external isolation remain reasoned. -- loopback-listeners (DEMONSTRATED; nomad, consul): Loopback runs used substitute TCP/Serf UDP ports; Consul DNS, WAN and gRPC were disabled, so their live listeners were not demonstrated. -- nomad-acl-probe (DEMONSTRATED; nomad): Loopback anonymous self/jobs/agent probes distinguished ACLs-off from ACLs-on; self remains 200 but changes AccessorID, while jobs and agent become 403. -- nomad-agent-acl (DEMONSTRATED; nomad): Server-handled paths can conceal a client with ACLs off; local /v1/agent/self exposed that client with 200 while the server returned 403. -- nomad-bootstrap-run (DEMONSTRATED; nomad): ACL probe rejection did not prove bootstrap complete; first bootstrap issued management credentials and the second returned already-done. -- nomad-anonymous-policy (DEMONSTRATED; nomad): Anonymous rights come from the named anonymous policy; historical exported-token inspection found it absent, then present with submit-job despite unchanged probe results. -- nomad-job-registration (DEMONSTRATED; nomad): Anonymous registration succeeded with ACLs off and failed with ACLs on and no anonymous policy; registration is not a demonstrated job execution. -- nomad-token-input (REASONED; nomad): CLI accepts -token or NOMAD_TOKEN, not stdin; the current prompted one-command prefix avoids argv/history but retains same-user/root environment visibility. That form has no recorded live Nomad run. -- consul-acl-probe (DEMONSTRATED; consul): Loopback ACLs-off gave self 401/agent 200; enabled allow gave self 403/agent 200; enabled deny gave agent 403 with anonymous-permission text. -- consul-agent-acl (DEMONSTRATED; consul): Each agent applies its own enabled/default_policy; mixed server/client configurations gave different results on their own APIs. -- consul-bootstrap-run (DEMONSTRATED; consul): Deny results appeared before bootstrap; first bootstrap issued a token and the second refused with bootstrap no longer allowed. -- consul-anonymous-token (DEMONSTRATED; consul): Inspect anonymous accessor 00000000-0000-0000-0000-000000000002 for policies, roles and identities; its recorded read listed none. -- consul-default-token (DEMONSTRATED; consul): An agent default token substitutes for anonymous requests; recorded key-write default token yielded self 200 and agent 403 yet allowed an unauthenticated KV write. -- consul-kv (DEMONSTRATED; consul): Anonymous KV writes succeeded with ACLs off and allow policy, and were refused under deny. -- consul-script-run (DEMONSTRATED; consul): With default script-check settings, remote script registration was refused. -- verify-tcp (REASONED; nomad, consul): Probe each actual public IP with a known-open control; connected establishes reachability, refused/timeout only local nonreachability. This guide cross-references another guide's run; UDP is not tested. -- verify-address-limits (REASONED; nomad, consul): TCP guard refuses zero/dotted-IPv6 forms but permits loopback and some hex forms reaching the probing host; use public addresses and corroborate surprising connections with host sockets. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Nomad schedules and runs workloads, and Consul holds a cluster's service catalog, health checks and key-value store. Both are controlled through an HTTP API that also serves a web UI, and both ship @@ -47116,107 +50491,183 @@ give the host's public address. A "connected" on a port you did not mean to expo - Nomad 2.0.7 agent debug endpoints when ACLs are disabled (`enable_debug`): https://github.com/hashicorp/nomad/blob/v2.0.7/acl/acl.go - Consul 2.0.4 ACL bootstrap: https://github.com/hashicorp/consul/blob/v2.0.4/agent/acl_endpoint.go and https://github.com/hashicorp/consul/blob/v2.0.4/agent/consul/acl_endpoint.go -====================================================================== -==> pocketbase.md -====================================================================== - -# Self-hosted backends: PocketBase and Appwrite +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/nomad-consul.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + **Version basis** -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. -Metadata reviewed 2026-10-19; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- pb: PocketBase (v0.40.4) -- appwrite: Appwrite (2.2.0) -- pb-docs: PocketBase documentation (rolling) (unknown) -- appwrite-docs: Appwrite documentation (rolling) (unknown) -- curl: curl (curl-8_14_1) -- utopia-php-database: utopia-php/database (7.3.9) +- nomad: Nomad Community Edition (2.0.7) +- consul: Consul Community Edition (2.0.4) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- pb-bootstrap (REASONED; pb, pb-docs): Privately bootstrap through the logged installer URL; superuser create EMAIL PASS exposes passwords in argv. Ordinary registration does not grant superuser rights. -- pb-installer (REASONED; pb): Installer system-superuser token lasts about 30 minutes; protect its URL and startup logs as credentials. -- pb-ips (REASONED; pb, pb-docs): Superuser IP allowlists are available from v0.38.0; superuserIPs defaults empty and restricts authenticated requests, not /_/ static assets. -- pb-tls (REASONED; pb): serve with a domain provides native HTTPS/ACME; alternatively terminate TLS at a reverse proxy. -- pb-bind (REASONED; pb): Without a domain serve defaults to 127.0.0.1:8090; with a domain it uses 0.0.0.0:80/443. Behind a local proxy omit the domain and set --http loopback. -- pb-origins (REASONED; pb): --origins defaults * and accepts comma-separated allowed origins; CORS does not authorize records. -- pb-proxy (REASONED; pb): Trust only headers the proxy overwrites, review useLeftmostIP ordering and block direct backend access to protect IP/rate controls. -- pb-dev (REASONED; pb): --dev adds diagnostics including SQL on stderr, not an auth bypass; its configured default comes from New(): true if os.Args[0] starts with a nonempty os.TempDir() or cacheDir() prefix, false otherwise. -- pb-cli (REASONED; pb): In the pinned CLI wrapper, Start() registers the serve and superuser commands on the root command and executes it. -- pb-rules (REASONED; pb, pb-docs): Each list/view/create/update/delete rule defaults null (superuser-only); empty opens to guests and nonempty filters. Superusers bypass rules. -- pb-manage (REASONED; pb): Top-level manageRule defaults null and permits privileged auth-record changes alongside create/update rules; its validator rejects empty-string rules. -- pb-files (REASONED; pb, pb-docs): File fields default unprotected despite locked record rules; Protected plus an authorized viewRule gates downloads, while a public viewRule still permits public access. -- pb-file-token (REASONED; pb, pb-docs): Short-lived file tokens provide identity context, not file authorization; viewRule decides. Protect token-bearing file URLs. -- pb-realtime (REASONED; pb, pb-docs): Collection subscriptions use listRule and individual-record subscriptions viewRule; SSE/subscription success does not prove event authorization. -- pb-password (REASONED; pb): Auth initialization enables password authentication with email as the identity field. -- pb-oauth (REASONED; pb, pb-docs): OAuth2 defaults disabled and is unsupported for _superusers. -- pb-mfa-default (REASONED; pb): MFA defaults disabled with duration 600 seconds. -- pb-otp-default (REASONED; pb): OTP defaults disabled with duration 180 seconds and length 8. -- pb-alerts (REASONED; pb): Authentication alerts default enabled; retain alerts and provide working email. -- pb-auth-token (REASONED; pb): Auth token duration defaults to 432000 seconds. -- pb-file-duration (REASONED; pb): File token duration defaults to 180 seconds. -- pb-auth-rule (REASONED; pb, pb-docs): authRule defaults empty; verified-only collections can require verified = true after email verification works. -- pb-mfa (REASONED; pb, pb-docs): For superusers retain password auth and enable both OTP and MFA; empty mfa.rule applies to everyone. OTP alone is not a second factor. -- pb-logout (REASONED; pb, pb-docs): Stateless authentication means clearing pb.authStore removes only the local copy, not a stolen token. -- pb-encryption (REASONED; pb, pb-docs): Settings default to plaintext JSON; select a 32-character environment secret with --encryptionEnv. This encrypts settings, not the whole database or files. -- pb-backups (REASONED; pb, pb-docs): Backups default local with empty cron (automatic backups off); archives contain main and logs databases plus remaining pb_data, excluding the backups directory and tracked exclusions; uploaded collection files on S3 require manual backup. Schedule/retain deliberately and protect archives. -- pb-backup-auth (REASONED; pb): Backup downloads require a superuser file token and obey superuser IP restrictions; ordinary auth/file tokens do not suffice. -- pb-limiter (REASONED; pb, pb-docs): Native limits exist from v0.23.0 but default disabled at v0.40.4; enable rateLimits.enabled. -- pb-limit-rules (REASONED; pb): Seeded limits are *:auth 2/3s, *:create 20/5s, /api/batch 3/1s and /api/ 300/10s. -- pb-limit-bypass (REASONED; pb): Superusers and rateLimits.excludedIPs bypass limits; test ordinary non-excluded clients. -- pb-smtp (REASONED; pb): smtp.enabled and smtp.tls default false; disabled SMTP falls back to sendmail. Configure protected transport and test actual delivery. -- appwrite-https (REASONED; appwrite, appwrite-docs): API FORCE_HTTPS and function/site ROUTER_FORCE_HTTPS default disabled at 2.2.0; explicitly enable both despite conflicting rolling deprecation/default documentation. -- appwrite-hosts (REASONED; appwrite): ROUTER_PROTECTION defaults disabled; configure actual domains and enable rejection of unknown hostnames. -- appwrite-registration (REASONED; appwrite): Console root-only registration defaults enabled with empty email/IP allowlists; these restrict account creation, not dashboard reachability or login. -- appwrite-console (REASONED; appwrite, appwrite-docs): Claim the first operator privately and use a separate network/access boundary for a private dashboard. -- appwrite-mfa (REASONED; appwrite, appwrite-docs): Enroll and verify operator TOTP, protect recovery codes; at 2.2.0 enabled MFA with a verified factor requires a second session factor. -- appwrite-dev-env (REASONED; appwrite): Development env disables root-only registration, abuse and router protection and carries placeholders; do not inherit it for production. -- appwrite-recreate (REASONED; appwrite-docs): Apply env/Compose changes with docker compose up -d from the install directory, then check effective behavior. -- appwrite-permissions (REASONED; appwrite, appwrite-docs): Collection and document grants are additive according to legacy documentation; documentSecurity defaults false in Appwrite 2.2.0 and must be enabled for document grants. The pinned parameter description states the need for one permission level. -- appwrite-creation (REASONED; appwrite, appwrite-docs, utopia-php-database): For null or omitted document permissions, the pinned aggregate helper preserves null before the handler assigns read/update/delete user grants to a nonprivileged requester with a user ID, or an empty list otherwise. API-key classification skips role validation but does not itself determine the default list. Use specific users/teams for private data. -- appwrite-read-path (REASONED; appwrite): Document GET classifies the requester from authorization roles (keys; owner/developer/admin privileged), reports disabled databases/collections as not found except to those requesters, and fetches the document through the database layer; collection permissions and documentSecurity are passed to the database layer at collection creation. -- appwrite-requester (REASONED; appwrite): With an API key the request role and scopes come from the key; a standard or ephemeral project key for the same project disables the default authorization status, as do admin-mode console sessions outside console project routes, while route scope validation applies to every requester. -- appwrite-keys (REASONED; appwrite, appwrite-docs): Properly scoped server API keys bypass resource permissions but still obey operation scopes; keys.write is admin-equivalent and keys never belong in clients. -- appwrite-encryption (REASONED; appwrite, appwrite-docs): Replace _APP_OPENSSL_KEY_V1=your-secret-key before production data; preserve the exact key separately, since replacement/loss strands encrypted secrets. -- appwrite-backups (REASONED; appwrite-docs): Back up database, persistent storage and deployment configuration; restrict access and test restoration in a separate installation. -- appwrite-abuse (REASONED; appwrite, appwrite-docs): _APP_OPTIONS_ABUSE defaults enabled but dev env disables it; server API-key requests are exempt, so test with ordinary clients. -- appwrite-smtp (REASONED; appwrite, appwrite-docs): SMTP host/port/secure/username/password default empty and empty host disables sending; configure the provider and confirm message receipt/use. -- appwrite-storage (REASONED; appwrite): Storage defaults local, upload limit 30000000 and server antivirus disabled; enabling scanning requires configured reachable ClamAV. -- appwrite-bucket (REASONED; appwrite): New bucket permissions are empty and fileSecurity=false; enable per-file security deliberately, since bucket grants also permit access. -- appwrite-bucket-options (REASONED; appwrite): Bucket encryption and antivirus default true; bucket antivirus does not enable the server scanner. -- appwrite-size (REASONED; appwrite): Encryption/scanning skip files above 20000000 bytes; cap maximumFileSize at that threshold when either is mandatory and test the boundary. -- appwrite-functions (REASONED; appwrite): Function execute roles and execution-key scopes default empty; grant narrowly. Disabled functions still admit authorized Server SDK API keys. -- appwrite-executor (REASONED; appwrite, appwrite-docs): Keep executor/orchestrator private, protect Docker-socket authority and replace _APP_EXECUTOR_SECRET; container execution is not demonstrated hostile-tenant isolation. -- verify-pb-records (REASONED; pb-docs, curl): Public rules allow guest reads; locked rules deny with 403, unsatisfied list filters can return empty 200 and view rules 404. Retain owner and separate superuser controls. -- verify-appwrite-records (REASONED; appwrite-docs): Broad collection grants defeat document restrictions; removing them must deny unrelated users while document grantees succeed. Compare client/server creation defaults. -- verify-appwrite-keys (REASONED; appwrite): A correctly scoped server key reads despite empty permissions, while a key without scope fails; neither substitutes for an ordinary-user control. -- verify-registration (REASONED; appwrite): On disposable Appwrite fixtures compare open signup 201 with root-only account-limit failure and independent email/IP restrictions; retain login/invitation controls. -- verify-pb-bootstrap (REASONED; pb): Guest and ordinary-user _superusers record creation must fail while a valid installer token creates the private operator; static dashboard assets prove no admin access. -- verify-appwrite-mfa (REASONED; appwrite, appwrite-docs): Console account GET succeeds without MFA, fails with user_more_factors_required for a fresh password-only MFA session, and succeeds after TOTP completion. -- verify-pb-writes (REASONED; pb): Compare intended/unauthorized POST/PATCH/DELETE and privileged auth-record management; locked manageRule grants no management and empty-string configuration must fail. -- verify-pb-files (REASONED; pb, curl): Before protection unsigned file GET works; Protected plus private viewRule rejects guests/unrelated-user file tokens with 404 while permitted-user bytes still arrive. -- verify-pb-realtime (REASONED; pb, pb-docs): Mutate a fixture while comparing collection and record subscriptions; deny unauthorized event delivery, retaining authorized delivery. PB_CONNECT/acknowledgment alone is insufficient. -- verify-pb-mfa (REASONED; pb-docs): Compare password-only completion with password-plus-OTP MFA and verified/unverified authRule logins; clearing authStore must not revoke a retained valid token. -- verify-pb-ips (REASONED; pb): Compare allowed/excluded superuser sources and forged forwarded headers; excluded sources must get 403 and direct backend access must fail with a working proxy control. -- verify-transport (REASONED; pb, appwrite): Compare HTTP/HTTPS and certificates, Appwrite API/function/site domains and configured/unknown Host; compare browser origins separately from record authorization. -- verify-abuse (REASONED; pb, appwrite-docs): Cross each configured threshold with ordinary non-excluded clients and compare disabled/enabled controls; PocketBase limiting yields 429 with later successful recovery. -- verify-email (REASONED; pb, appwrite-docs): Request OTP, verification and recovery messages and confirm receipt and use; request success alone is not delivery evidence. -- verify-pb-recovery (REASONED; pb, pb-docs): Compare plaintext/encrypted persisted settings, restart with retained key and restore local/S3 data; only permitted superuser file tokens may download backups. -- verify-appwrite-recovery (REASONED; appwrite-docs): Restore encrypted fixtures with the original key; a separate wrong-key fixture must not recover encrypted values. -- verify-uploads (REASONED; appwrite): Compare permitted/unrelated uploads and size-boundary fixtures; mandatory processing requires rejecting over-limit files and confirming accepted-file encryption/scanning. -- verify-functions (REASONED; appwrite): Compare intended execute roles, unrelated users and privileged server keys, including disabled functions; confirm executor/orchestrator privacy from a second host. -- verify-bundle (REASONED; pb-docs, appwrite-docs): Search filenames using protected exact-secret patterns with a disposable marker control; client bundles must contain neither superuser tokens nor Appwrite API keys, and encoded/split forms can evade scanning. -- local-guards (DEMONSTRATED; pb-docs, appwrite-docs): Recorded guard tests rejected placeholders and omitted/shortened arguments; this demonstrates shell behavior only, not service access or deployed-bundle scanning. -- local-syntax (DEMONSTRATED; appwrite, pb-docs, appwrite-docs): All three bash fences have recorded local lint/syntax success, not live service evidence; ShellCheck version is recorded only in the body. -- source-limits (REASONED; appwrite-docs, pb-docs, appwrite): The PocketBase CLI wrapper, --dev default, and Appwrite requester classification and document-read call sites are now cited; how utopia-php/database matches roles against collection/document permissions (including documentSecurity) and what the disabled default authorization status bypasses remain untraced in that unfetched dependency. +- nomad-bind (REASONED; nomad): Nomad defaults bind_addr=0.0.0.0 and all three listeners fall back to it; setting only addresses.http leaves RPC and gossip on bind_addr. +- nomad-http (REASONED; nomad): Nomad HTTP API/UI uses 4646; UI defaults enabled at /ui/. +- nomad-rpc (REASONED; nomad): Nomad RPC uses 4647; keep it on private or management addresses. +- nomad-gossip (REASONED; nomad): Nomad Serf uses 4648 TCP/UDP; gossip is unencrypted until encrypt is configured. +- nomad-dev (REASONED; nomad): Linux -dev binds 127.0.0.1 except -dev-connect, which binds 0.0.0.0. Wildcard behavior was not observed. +- nomad-acl-default (REASONED; nomad): ACLs default disabled; enable acl.enabled on every agent before exposing the API. +- nomad-debug (REASONED; nomad): Debug endpoints still require enable_debug with ACLs off; -dev enables it. +- nomad-exec (REASONED; nomad): On a root Linux client, exec uses a chroot and task user, default nobody, with root refused by the default exec denylist; no client job was run. +- nomad-raw-exec (REASONED; nomad): raw_exec has no isolation and defaults off, but -dev enables it. +- nomad-remote-exec (REASONED; nomad): Remote task execution defaults enabled through disable_remote_exec=false. +- nomad-tls (REASONED; nomad): HTTP/RPC TLS defaults off; enable both tls.http and tls.rpc. +- nomad-bootstrap-reset (REASONED; nomad): Unauthenticated PUT /v1/acl/bootstrap claims the first management token after ACL enablement; repeat is blocked until the operator reset file in data_dir/server. +- consul-client-bind (REASONED; consul): Consul client_addr defaults 127.0.0.1; widening it or addresses.http makes the API reachable. +- consul-http (REASONED; consul): Consul HTTP API is 8500; UI defaults off and -dev enables /ui/ on that port. +- consul-dns (REASONED; consul): Consul DNS uses 8600 TCP/UDP on client_addr. +- consul-rpc (REASONED; consul): Consul server RPC uses 8300 on bind_addr, default 0.0.0.0. +- consul-lan (REASONED; consul): Consul LAN Serf uses 8301 TCP/UDP on bind_addr. +- consul-wan (REASONED; consul): Consul WAN Serf uses 8302 TCP/UDP on bind_addr. +- consul-https (REASONED; consul): Consul HTTPS defaults disabled. +- consul-grpc (REASONED; consul): Plaintext gRPC defaults disabled; servers open TLS gRPC on 8503 on client_addr. +- consul-acl-default (REASONED; consul): Consul ACLs default off and disabled ACL resolution grants management rights; with ACLs enabled default_policy still defaults allow. +- consul-bootstrap-reset (REASONED; consul): Bootstrap is unauthenticated and one-time until an operator writes acl-bootstrap-reset; bootstrap privately before exposing the API. +- consul-script (REASONED; consul): enable_script_checks defaults off; enabling without ACLs or allow_write_http_from only warns. Prefer enable_local_script_checks if needed. +- consul-remote-exec (REASONED; consul): consul exec defaults disabled through disable_remote_exec=true. +- consul-tls-gossip (REASONED; consul): TLS verification defaults off and gossip is unencrypted without encrypt. +- verify-inventory (REASONED; nomad, consul): Inventory TCP/UDP on private or management addresses; wildcard exposed binds and actual external isolation remain reasoned. +- loopback-listeners (DEMONSTRATED; nomad, consul): Loopback runs used substitute TCP/Serf UDP ports; Consul DNS, WAN and gRPC were disabled, so their live listeners were not demonstrated. +- nomad-acl-probe (DEMONSTRATED; nomad): Loopback anonymous self/jobs/agent probes distinguished ACLs-off from ACLs-on; self remains 200 but changes AccessorID, while jobs and agent become 403. +- nomad-agent-acl (DEMONSTRATED; nomad): Server-handled paths can conceal a client with ACLs off; local /v1/agent/self exposed that client with 200 while the server returned 403. +- nomad-bootstrap-run (DEMONSTRATED; nomad): ACL probe rejection did not prove bootstrap complete; first bootstrap issued management credentials and the second returned already-done. +- nomad-anonymous-policy (DEMONSTRATED; nomad): Anonymous rights come from the named anonymous policy; historical exported-token inspection found it absent, then present with submit-job despite unchanged probe results. +- nomad-job-registration (DEMONSTRATED; nomad): Anonymous registration succeeded with ACLs off and failed with ACLs on and no anonymous policy; registration is not a demonstrated job execution. +- nomad-token-input (REASONED; nomad): CLI accepts -token or NOMAD_TOKEN, not stdin; the current prompted one-command prefix avoids argv/history but retains same-user/root environment visibility. That form has no recorded live Nomad run. +- consul-acl-probe (DEMONSTRATED; consul): Loopback ACLs-off gave self 401/agent 200; enabled allow gave self 403/agent 200; enabled deny gave agent 403 with anonymous-permission text. +- consul-agent-acl (DEMONSTRATED; consul): Each agent applies its own enabled/default_policy; mixed server/client configurations gave different results on their own APIs. +- consul-bootstrap-run (DEMONSTRATED; consul): Deny results appeared before bootstrap; first bootstrap issued a token and the second refused with bootstrap no longer allowed. +- consul-anonymous-token (DEMONSTRATED; consul): Inspect anonymous accessor 00000000-0000-0000-0000-000000000002 for policies, roles and identities; its recorded read listed none. +- consul-default-token (DEMONSTRATED; consul): An agent default token substitutes for anonymous requests; recorded key-write default token yielded self 200 and agent 403 yet allowed an unauthenticated KV write. +- consul-kv (DEMONSTRATED; consul): Anonymous KV writes succeeded with ACLs off and allow policy, and were refused under deny. +- consul-script-run (DEMONSTRATED; consul): With default script-check settings, remote script registration was refused. +- verify-tcp (REASONED; nomad, consul): Probe each actual public IP with a known-open control; connected establishes reachability, refused/timeout only local nonreachability. This guide cross-references another guide's run; UDP is not tested. +- verify-address-limits (REASONED; nomad, consul): TCP guard refuses zero/dotted-IPv6 forms but permits loopback and some hex forms reaching the probing host; use public addresses and corroborate surprising connections with host sockets. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "05ae6961d3225ec0e28ef043428dfeea38f86a7be4cfd93f6a6d58b4990f084a", + "components": { + "nomad": { + "name": "Nomad Community Edition", + "basis": "2.0.7", + "sources": { + "s52d80d2821ed": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/agent/config.go", + "sa1c2951d3ca6": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/config/ui.go", + "s553448c7957c": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/auth/auth.go", + "s7496bcf129dc": "https://github.com/hashicorp/nomad/blob/v2.0.7/acl/acl.go", + "sd665f15bc452": "https://github.com/hashicorp/nomad/blob/v2.0.7/drivers/exec/driver.go", + "sd5c86223bdf5": "https://github.com/hashicorp/nomad/blob/v2.0.7/client/config/config.go", + "sd5f2c389c6ae": "https://github.com/hashicorp/nomad/blob/v2.0.7/drivers/rawexec/driver.go", + "se7ba8e4262dc": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/config/tls.go", + "sc3e36e830b67": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/acl_endpoint.go", + "s754c582b7565": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/agent/agent.go", + "s8193264e48c2": "https://github.com/hashicorp/nomad/blob/v2.0.7/nomad/structs/acl.go", + "sba39c1a9119a": "https://github.com/hashicorp/nomad/blob/v2.0.7/command/meta.go#L96", + "sa04bf8c95cb6": "https://github.com/hashicorp/nomad/blob/v2.0.7/api/api.go#L388-L389" + } + }, + "consul": { + "name": "Consul Community Edition", + "basis": "2.0.4", + "sources": { + "s679a306e1b08": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/config/default.go", + "s8a0b5abac641": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/config/builder.go", + "sc4fb30ca3702": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/consul/acl.go", + "s35b783d75794": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/acl_endpoint.go", + "sa77598ec3695": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/consul/acl_endpoint.go", + "s00748de44382": "https://github.com/hashicorp/consul/blob/v2.0.4/acl/acl.go", + "sab8a5866d9c9": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/http.go", + "sfeb50129fc10": "https://github.com/hashicorp/consul/blob/v2.0.4/agent/token/persistence.go" + } + } + }, + "claims": { + "nomad-bind": {"text": "Nomad defaults bind_addr=0.0.0.0 and all three listeners fall back to it; setting only addresses.http leaves RPC and gossip on bind_addr.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-http": {"text": "Nomad HTTP API/UI uses 4646; UI defaults enabled at /ui/.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:sa1c2951d3ca6"], "status": "REASONED"}, + "nomad-rpc": {"text": "Nomad RPC uses 4647; keep it on private or management addresses.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-gossip": {"text": "Nomad Serf uses 4648 TCP/UDP; gossip is unencrypted until encrypt is configured.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-dev": {"text": "Linux -dev binds 127.0.0.1 except -dev-connect, which binds 0.0.0.0. Wildcard behavior was not observed.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-acl-default": {"text": "ACLs default disabled; enable acl.enabled on every agent before exposing the API.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s553448c7957c", "nomad:s7496bcf129dc"], "status": "REASONED"}, + "nomad-debug": {"text": "Debug endpoints still require enable_debug with ACLs off; -dev enables it.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s7496bcf129dc"], "status": "REASONED"}, + "nomad-exec": {"text": "On a root Linux client, exec uses a chroot and task user, default nobody, with root refused by the default exec denylist; no client job was run.", "components": ["nomad"], "sources": ["nomad:sd665f15bc452", "nomad:sd5c86223bdf5"], "status": "REASONED"}, + "nomad-raw-exec": {"text": "raw_exec has no isolation and defaults off, but -dev enables it.", "components": ["nomad"], "sources": ["nomad:sd5f2c389c6ae", "nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-remote-exec": {"text": "Remote task execution defaults enabled through disable_remote_exec=false.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed"], "status": "REASONED"}, + "nomad-tls": {"text": "HTTP/RPC TLS defaults off; enable both tls.http and tls.rpc.", "components": ["nomad"], "sources": ["nomad:se7ba8e4262dc"], "status": "REASONED"}, + "nomad-bootstrap-reset": {"text": "Unauthenticated PUT /v1/acl/bootstrap claims the first management token after ACL enablement; repeat is blocked until the operator reset file in data_dir/server.", "components": ["nomad"], "sources": ["nomad:sc3e36e830b67", "nomad:s754c582b7565"], "status": "REASONED"}, + "consul-client-bind": {"text": "Consul client_addr defaults 127.0.0.1; widening it or addresses.http makes the API reachable.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-http": {"text": "Consul HTTP API is 8500; UI defaults off and -dev enables /ui/ on that port.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-dns": {"text": "Consul DNS uses 8600 TCP/UDP on client_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-rpc": {"text": "Consul server RPC uses 8300 on bind_addr, default 0.0.0.0.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-lan": {"text": "Consul LAN Serf uses 8301 TCP/UDP on bind_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-wan": {"text": "Consul WAN Serf uses 8302 TCP/UDP on bind_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-https": {"text": "Consul HTTPS defaults disabled.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-grpc": {"text": "Plaintext gRPC defaults disabled; servers open TLS gRPC on 8503 on client_addr.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-acl-default": {"text": "Consul ACLs default off and disabled ACL resolution grants management rights; with ACLs enabled default_policy still defaults allow.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641", "consul:sc4fb30ca3702"], "status": "REASONED"}, + "consul-bootstrap-reset": {"text": "Bootstrap is unauthenticated and one-time until an operator writes acl-bootstrap-reset; bootstrap privately before exposing the API.", "components": ["consul"], "sources": ["consul:s35b783d75794", "consul:sa77598ec3695"], "status": "REASONED"}, + "consul-script": {"text": "enable_script_checks defaults off; enabling without ACLs or allow_write_http_from only warns. Prefer enable_local_script_checks if needed.", "components": ["consul"], "sources": ["consul:s8a0b5abac641"], "status": "REASONED"}, + "consul-remote-exec": {"text": "consul exec defaults disabled through disable_remote_exec=true.", "components": ["consul"], "sources": ["consul:s679a306e1b08"], "status": "REASONED"}, + "consul-tls-gossip": {"text": "TLS verification defaults off and gossip is unencrypted without encrypt.", "components": ["consul"], "sources": ["consul:s679a306e1b08", "consul:s8a0b5abac641"], "status": "REASONED"}, + "verify-inventory": {"text": "Inventory TCP/UDP on private or management addresses; wildcard exposed binds and actual external isolation remain reasoned.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [1]}, + "loopback-listeners": {"text": "Loopback runs used substitute TCP/Serf UDP ports; Consul DNS, WAN and gRPC were disabled, so their live listeners were not demonstrated.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "DEMONSTRATED", "evidence": "On the loopback run, the agents listened on TCP on test ports standing in for Nomad's 4646, 4647 and 4648 and Consul's 8500, 8300 and 8301 (the runs turned off Consul's DNS, Serf WAN and gRPC), and on UDP on the two Serf ports."}, + "nomad-acl-probe": {"text": "Loopback anonymous self/jobs/agent probes distinguished ACLs-off from ACLs-on; self remains 200 but changes AccessorID, while jobs and agent become 403.", "components": ["nomad"], "sources": ["nomad:s553448c7957c", "nomad:s7496bcf129dc", "nomad:s8193264e48c2"], "status": "DEMONSTRATED", "evidence": "All of these outcomes were observed with the curl probe block and these commands, on agents listening on test ports.", "verify": [2]}, + "nomad-agent-acl": {"text": "Server-handled paths can conceal a client with ACLs off; local /v1/agent/self exposed that client with 200 while the server returned 403.", "components": ["nomad"], "sources": ["nomad:s52d80d2821ed", "nomad:s7496bcf129dc"], "status": "DEMONSTRATED", "evidence": "In a cluster whose server had ACLs on and was bootstrapped, a client agent configured with `acl { enabled = false }` answered the first two paths with the ACLs-on results and `/v1/agent/self` with `200`, while the server answered it with `403`.", "verify": [2]}, + "nomad-bootstrap-run": {"text": "ACL probe rejection did not prove bootstrap complete; first bootstrap issued management credentials and the second returned already-done.", "components": ["nomad"], "sources": ["nomad:sc3e36e830b67"], "status": "DEMONSTRATED", "evidence": "Before bootstrap, the probe already printed the ACLs-on results; the first `nomad acl bootstrap` printed a management token and the second failed with `Unexpected response code: 400 (ACL bootstrap already done ...)`."}, + "nomad-anonymous-policy": {"text": "Anonymous rights come from the named anonymous policy; historical exported-token inspection found it absent, then present with submit-job despite unchanged probe results.", "components": ["nomad"], "sources": ["nomad:s8193264e48c2"], "status": "DEMONSTRATED", "evidence": "After an `anonymous` policy granting only `submit-job` was applied, the probe's results did not change, an anonymous job registration returned `200`, and `nomad acl policy info anonymous`, run in the same earlier form, showed the policy."}, + "nomad-job-registration": {"text": "Anonymous registration succeeded with ACLs off and failed with ACLs on and no anonymous policy; registration is not a demonstrated job execution.", "components": ["nomad"], "sources": ["nomad:s553448c7957c", "nomad:s7496bcf129dc"], "status": "DEMONSTRATED", "evidence": "With ACLs off, an anonymous job registration also returned `200`; with ACLs on and no `anonymous` policy, it returned `403`."}, + "nomad-token-input": {"text": "CLI accepts -token or NOMAD_TOKEN, not stdin; the current prompted one-command prefix avoids argv/history but retains same-user/root environment visibility. That form has no recorded live Nomad run.", "components": ["nomad"], "sources": ["nomad:sba39c1a9119a", "nomad:sa04bf8c95cb6"], "status": "REASONED", "verify": [3]}, + "consul-acl-probe": {"text": "Loopback ACLs-off gave self 401/agent 200; enabled allow gave self 403/agent 200; enabled deny gave agent 403 with anonymous-permission text.", "components": ["consul"], "sources": ["consul:sc4fb30ca3702", "consul:s8a0b5abac641"], "status": "DEMONSTRATED", "evidence": "All of these outcomes were observed with this block and these commands, on agents listening on test ports.", "verify": [4]}, + "consul-agent-acl": {"text": "Each agent applies its own enabled/default_policy; mixed server/client configurations gave different results on their own APIs.", "components": ["consul"], "sources": ["consul:s8a0b5abac641", "consul:sc4fb30ca3702"], "status": "DEMONSTRATED", "evidence": "with the server on the `allow` policy and the client on `deny`, it printed `200` on `/v1/agent/self` against the server and the deny results against the client.", "verify": [4]}, + "consul-bootstrap-run": {"text": "Deny results appeared before bootstrap; first bootstrap issued a token and the second refused with bootstrap no longer allowed.", "components": ["consul"], "sources": ["consul:s35b783d75794", "consul:sa77598ec3695"], "status": "DEMONSTRATED", "evidence": "the first `consul acl bootstrap` printed a token and the second failed with `Unexpected response code: 403 (Permission denied: ACL bootstrap no longer allowed ...)`"}, + "consul-anonymous-token": {"text": "Inspect anonymous accessor 00000000-0000-0000-0000-000000000002 for policies, roles and identities; its recorded read listed none.", "components": ["consul"], "sources": ["consul:s00748de44382"], "status": "DEMONSTRATED", "evidence": "the anonymous token's read listed none of those sections."}, + "consul-default-token": {"text": "An agent default token substitutes for anonymous requests; recorded key-write default token yielded self 200 and agent 403 yet allowed an unauthenticated KV write.", "components": ["consul"], "sources": ["consul:sab8a5866d9c9", "consul:sfeb50129fc10"], "status": "DEMONSTRATED", "evidence": "On the loopback run, a default token holding key write, set through the agent's token API on a deny-policy agent, made the block print `200` on `/v1/acl/token/self` and a `403` on `/v1/agent/self` without the anonymous-token message, while a key-value write without a token succeeded.", "verify": [4]}, + "consul-kv": {"text": "Anonymous KV writes succeeded with ACLs off and allow policy, and were refused under deny.", "components": ["consul"], "sources": ["consul:sc4fb30ca3702", "consul:s679a306e1b08"], "status": "DEMONSTRATED", "evidence": "On the same runs an anonymous key-value write succeeded in both exposed states and was refused with `403` with the deny policy"}, + "consul-script-run": {"text": "With default script-check settings, remote script registration was refused.", "components": ["consul"], "sources": ["consul:s8a0b5abac641"], "status": "DEMONSTRATED", "evidence": "with script checks at their default, registering a script check through the API was refused with \"Scripts are disabled on this agent from remote calls\"."}, + "verify-tcp": {"text": "Probe each actual public IP with a known-open control; connected establishes reachability, refused/timeout only local nonreachability. This guide cross-references another guide's run; UDP is not tested.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [5]}, + "verify-address-limits": {"text": "TCP guard refuses zero/dotted-IPv6 forms but permits loopback and some hex forms reaching the probing host; use public addresses and corroborate surprising connections with host sockets.", "components": ["nomad", "consul"], "sources": ["nomad:s52d80d2821ed", "consul:s679a306e1b08"], "status": "REASONED", "verify": [5]} + } +} +```` +====================================================================== +==> pocketbase.md +====================================================================== + +# Self-hosted backends: PocketBase and Appwrite + +Written against PocketBase v0.40.4, Appwrite 2.2.0, utopia-php/database 7.3.9, and curl curl-8_14_1; 2 further sources carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 75 claims: 2 demonstrated by a check that ran and 73 reasoned from cited sources. + +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. + Like Firebase and Supabase ([firebase-supabase.md](firebase-supabase.md)), these backends hand a public API endpoint to your client code; the collection or resource rules you write gate access to your data. Files and privileged server credentials need separate attention. Both also ship @@ -47905,6 +51356,307 @@ service result or deployed-bundle scan is claimed. - [curl curl-8_14_1 headers from stdin (checked October 2026)](https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md). - [curl curl-8_14_1 configuration from stdin (checked October 2026)](https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/config.md). + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/pocketbase.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- pb: PocketBase (v0.40.4) +- appwrite: Appwrite (2.2.0) +- pb-docs: PocketBase documentation (rolling) (unknown) +- appwrite-docs: Appwrite documentation (rolling) (unknown) +- utopia-php-database: utopia-php/database (7.3.9) +- curl: curl (curl-8_14_1) + +Claims (full source mapping in the JSON block below): + +- pb-bootstrap (REASONED; pb, pb-docs): Privately bootstrap through the logged installer URL; superuser create EMAIL PASS exposes passwords in argv. Ordinary registration does not grant superuser rights. +- pb-installer (REASONED; pb): Installer system-superuser token lasts about 30 minutes; protect its URL and startup logs as credentials. +- pb-ips (REASONED; pb, pb-docs): Superuser IP allowlists are available from v0.38.0; superuserIPs defaults empty and restricts authenticated requests, not /_/ static assets. +- pb-tls (REASONED; pb): serve with a domain provides native HTTPS/ACME; alternatively terminate TLS at a reverse proxy. +- pb-bind (REASONED; pb): Without a domain serve defaults to 127.0.0.1:8090; with a domain it uses 0.0.0.0:80/443. Behind a local proxy omit the domain and set --http loopback. +- pb-origins (REASONED; pb): --origins defaults * and accepts comma-separated allowed origins; CORS does not authorize records. +- pb-proxy (REASONED; pb): Trust only headers the proxy overwrites, review useLeftmostIP ordering and block direct backend access to protect IP/rate controls. +- pb-dev (REASONED; pb): --dev adds diagnostics including SQL on stderr, not an auth bypass; its configured default comes from New(): true if os.Args[0] starts with a nonempty os.TempDir() or cacheDir() prefix, false otherwise. +- pb-cli (REASONED; pb): In the pinned CLI wrapper, Start() registers the serve and superuser commands on the root command and executes it. +- pb-rules (REASONED; pb, pb-docs): Each list/view/create/update/delete rule defaults null (superuser-only); empty opens to guests and nonempty filters. Superusers bypass rules. +- pb-manage (REASONED; pb): Top-level manageRule defaults null and permits privileged auth-record changes alongside create/update rules; its validator rejects empty-string rules. +- pb-files (REASONED; pb, pb-docs): File fields default unprotected despite locked record rules; Protected plus an authorized viewRule gates downloads, while a public viewRule still permits public access. +- pb-file-token (REASONED; pb, pb-docs): Short-lived file tokens provide identity context, not file authorization; viewRule decides. Protect token-bearing file URLs. +- pb-realtime (REASONED; pb, pb-docs): Collection subscriptions use listRule and individual-record subscriptions viewRule; SSE/subscription success does not prove event authorization. +- pb-password (REASONED; pb): Auth initialization enables password authentication with email as the identity field. +- pb-oauth (REASONED; pb, pb-docs): OAuth2 defaults disabled and is unsupported for _superusers. +- pb-mfa-default (REASONED; pb): MFA defaults disabled with duration 600 seconds. +- pb-otp-default (REASONED; pb): OTP defaults disabled with duration 180 seconds and length 8. +- pb-alerts (REASONED; pb): Authentication alerts default enabled; retain alerts and provide working email. +- pb-auth-token (REASONED; pb): Auth token duration defaults to 432000 seconds. +- pb-file-duration (REASONED; pb): File token duration defaults to 180 seconds. +- pb-auth-rule (REASONED; pb, pb-docs): authRule defaults empty; verified-only collections can require verified = true after email verification works. +- pb-mfa (REASONED; pb, pb-docs): For superusers retain password auth and enable both OTP and MFA; empty mfa.rule applies to everyone. OTP alone is not a second factor. +- pb-logout (REASONED; pb, pb-docs): Stateless authentication means clearing pb.authStore removes only the local copy, not a stolen token. +- pb-encryption (REASONED; pb, pb-docs): Settings default to plaintext JSON; select a 32-character environment secret with --encryptionEnv. This encrypts settings, not the whole database or files. +- pb-backups (REASONED; pb, pb-docs): Backups default local with empty cron (automatic backups off); archives contain main and logs databases plus remaining pb_data, excluding the backups directory and tracked exclusions; uploaded collection files on S3 require manual backup. Schedule/retain deliberately and protect archives. +- pb-backup-auth (REASONED; pb): Backup downloads require a superuser file token and obey superuser IP restrictions; ordinary auth/file tokens do not suffice. +- pb-limiter (REASONED; pb, pb-docs): Native limits exist from v0.23.0 but default disabled at v0.40.4; enable rateLimits.enabled. +- pb-limit-rules (REASONED; pb): Seeded limits are *:auth 2/3s, *:create 20/5s, /api/batch 3/1s and /api/ 300/10s. +- pb-limit-bypass (REASONED; pb): Superusers and rateLimits.excludedIPs bypass limits; test ordinary non-excluded clients. +- pb-smtp (REASONED; pb): smtp.enabled and smtp.tls default false; disabled SMTP falls back to sendmail. Configure protected transport and test actual delivery. +- appwrite-https (REASONED; appwrite, appwrite-docs): API FORCE_HTTPS and function/site ROUTER_FORCE_HTTPS default disabled at 2.2.0; explicitly enable both despite conflicting rolling deprecation/default documentation. +- appwrite-hosts (REASONED; appwrite): ROUTER_PROTECTION defaults disabled; configure actual domains and enable rejection of unknown hostnames. +- appwrite-registration (REASONED; appwrite): Console root-only registration defaults enabled with empty email/IP allowlists; these restrict account creation, not dashboard reachability or login. +- appwrite-console (REASONED; appwrite, appwrite-docs): Claim the first operator privately and use a separate network/access boundary for a private dashboard. +- appwrite-mfa (REASONED; appwrite, appwrite-docs): Enroll and verify operator TOTP, protect recovery codes; at 2.2.0 enabled MFA with a verified factor requires a second session factor. +- appwrite-dev-env (REASONED; appwrite): Development env disables root-only registration, abuse and router protection and carries placeholders; do not inherit it for production. +- appwrite-recreate (REASONED; appwrite-docs): Apply env/Compose changes with docker compose up -d from the install directory, then check effective behavior. +- appwrite-permissions (REASONED; appwrite, appwrite-docs): Collection and document grants are additive according to legacy documentation; documentSecurity defaults false in Appwrite 2.2.0 and must be enabled for document grants. The pinned parameter description states the need for one permission level. +- appwrite-creation (REASONED; appwrite, appwrite-docs, utopia-php-database): For null or omitted document permissions, the pinned aggregate helper preserves null before the handler assigns read/update/delete user grants to a nonprivileged requester with a user ID, or an empty list otherwise. API-key classification skips role validation but does not itself determine the default list. Use specific users/teams for private data. +- appwrite-read-path (REASONED; appwrite): Document GET classifies the requester from authorization roles (keys; owner/developer/admin privileged), reports disabled databases/collections as not found except to those requesters, and fetches the document through the database layer; collection permissions and documentSecurity are passed to the database layer at collection creation. +- appwrite-requester (REASONED; appwrite): With an API key the request role and scopes come from the key; a standard or ephemeral project key for the same project disables the default authorization status, as do admin-mode console sessions outside console project routes, while route scope validation applies to every requester. +- appwrite-keys (REASONED; appwrite, appwrite-docs): Properly scoped server API keys bypass resource permissions but still obey operation scopes; keys.write is admin-equivalent and keys never belong in clients. +- appwrite-encryption (REASONED; appwrite, appwrite-docs): Replace _APP_OPENSSL_KEY_V1=your-secret-key before production data; preserve the exact key separately, since replacement/loss strands encrypted secrets. +- appwrite-backups (REASONED; appwrite-docs): Back up database, persistent storage and deployment configuration; restrict access and test restoration in a separate installation. +- appwrite-abuse (REASONED; appwrite, appwrite-docs): _APP_OPTIONS_ABUSE defaults enabled but dev env disables it; server API-key requests are exempt, so test with ordinary clients. +- appwrite-smtp (REASONED; appwrite, appwrite-docs): SMTP host/port/secure/username/password default empty and empty host disables sending; configure the provider and confirm message receipt/use. +- appwrite-storage (REASONED; appwrite): Storage defaults local, upload limit 30000000 and server antivirus disabled; enabling scanning requires configured reachable ClamAV. +- appwrite-bucket (REASONED; appwrite): New bucket permissions are empty and fileSecurity=false; enable per-file security deliberately, since bucket grants also permit access. +- appwrite-bucket-options (REASONED; appwrite): Bucket encryption and antivirus default true; bucket antivirus does not enable the server scanner. +- appwrite-size (REASONED; appwrite): Encryption/scanning skip files above 20000000 bytes; cap maximumFileSize at that threshold when either is mandatory and test the boundary. +- appwrite-functions (REASONED; appwrite): Function execute roles and execution-key scopes default empty; grant narrowly. Disabled functions still admit authorized Server SDK API keys. +- appwrite-executor (REASONED; appwrite, appwrite-docs): Keep executor/orchestrator private, protect Docker-socket authority and replace _APP_EXECUTOR_SECRET; container execution is not demonstrated hostile-tenant isolation. +- verify-pb-records (REASONED; pb-docs, curl): Public rules allow guest reads; locked rules deny with 403, unsatisfied list filters can return empty 200 and view rules 404. Retain owner and separate superuser controls. +- verify-appwrite-records (REASONED; appwrite-docs): Broad collection grants defeat document restrictions; removing them must deny unrelated users while document grantees succeed. Compare client/server creation defaults. +- verify-appwrite-keys (REASONED; appwrite): A correctly scoped server key reads despite empty permissions, while a key without scope fails; neither substitutes for an ordinary-user control. +- verify-registration (REASONED; appwrite): On disposable Appwrite fixtures compare open signup 201 with root-only account-limit failure and independent email/IP restrictions; retain login/invitation controls. +- verify-pb-bootstrap (REASONED; pb): Guest and ordinary-user _superusers record creation must fail while a valid installer token creates the private operator; static dashboard assets prove no admin access. +- verify-appwrite-mfa (REASONED; appwrite, appwrite-docs): Console account GET succeeds without MFA, fails with user_more_factors_required for a fresh password-only MFA session, and succeeds after TOTP completion. +- verify-pb-writes (REASONED; pb): Compare intended/unauthorized POST/PATCH/DELETE and privileged auth-record management; locked manageRule grants no management and empty-string configuration must fail. +- verify-pb-files (REASONED; pb, curl): Before protection unsigned file GET works; Protected plus private viewRule rejects guests/unrelated-user file tokens with 404 while permitted-user bytes still arrive. +- verify-pb-realtime (REASONED; pb, pb-docs): Mutate a fixture while comparing collection and record subscriptions; deny unauthorized event delivery, retaining authorized delivery. PB_CONNECT/acknowledgment alone is insufficient. +- verify-pb-mfa (REASONED; pb-docs): Compare password-only completion with password-plus-OTP MFA and verified/unverified authRule logins; clearing authStore must not revoke a retained valid token. +- verify-pb-ips (REASONED; pb): Compare allowed/excluded superuser sources and forged forwarded headers; excluded sources must get 403 and direct backend access must fail with a working proxy control. +- verify-transport (REASONED; pb, appwrite): Compare HTTP/HTTPS and certificates, Appwrite API/function/site domains and configured/unknown Host; compare browser origins separately from record authorization. +- verify-abuse (REASONED; pb, appwrite-docs): Cross each configured threshold with ordinary non-excluded clients and compare disabled/enabled controls; PocketBase limiting yields 429 with later successful recovery. +- verify-email (REASONED; pb, appwrite-docs): Request OTP, verification and recovery messages and confirm receipt and use; request success alone is not delivery evidence. +- verify-pb-recovery (REASONED; pb, pb-docs): Compare plaintext/encrypted persisted settings, restart with retained key and restore local/S3 data; only permitted superuser file tokens may download backups. +- verify-appwrite-recovery (REASONED; appwrite-docs): Restore encrypted fixtures with the original key; a separate wrong-key fixture must not recover encrypted values. +- verify-uploads (REASONED; appwrite): Compare permitted/unrelated uploads and size-boundary fixtures; mandatory processing requires rejecting over-limit files and confirming accepted-file encryption/scanning. +- verify-functions (REASONED; appwrite): Compare intended execute roles, unrelated users and privileged server keys, including disabled functions; confirm executor/orchestrator privacy from a second host. +- verify-bundle (REASONED; pb-docs, appwrite-docs): Search filenames using protected exact-secret patterns with a disposable marker control; client bundles must contain neither superuser tokens nor Appwrite API keys, and encoded/split forms can evade scanning. +- local-guards (DEMONSTRATED; pb-docs, appwrite-docs): Recorded guard tests rejected placeholders and omitted/shortened arguments; this demonstrates shell behavior only, not service access or deployed-bundle scanning. +- local-syntax (DEMONSTRATED; appwrite, pb-docs, appwrite-docs): All three bash fences have recorded local lint/syntax success, not live service evidence; ShellCheck version is recorded only in the body. +- source-limits (REASONED; appwrite-docs, pb-docs, appwrite): The PocketBase CLI wrapper, --dev default, and Appwrite requester classification and document-read call sites are now cited; how utopia-php/database matches roles against collection/document permissions (including documentSecurity) and what the disabled default authorization status bypasses remain untraced in that unfetched dependency. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "56cf568348e9a9e7890afd9133b526447b70bf593b07da5b93bedb63ce116e85", + "components": { + "pb": { + "name": "PocketBase", + "basis": "v0.40.4", + "sources": { + "saf684a26056b": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/installer.go", + "s4b155ef30262": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/cmd/serve.go", + "sb2129128b86c": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/serve.go", + "s5b66ca69bf6d": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/middlewares.go", + "s04ae861b5709": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/record_query.go", + "s69aa223f52e4": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/record_crud.go", + "sce114368c2ad": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/collection_model_auth_options.go", + "s03dd7dd84c2a": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/settings_model.go", + "seb56d3763223": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/core/base.go", + "sa5e571f8d473": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/file.go", + "sfd2f779e8e11": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/realtime.go", + "s31a5adccd695": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/backup.go", + "se74188862a55": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/backup_create.go#L36-L60", + "sf6ffdca4dd39": "https://raw.githubusercontent.com/pocketbase/pocketbase/v0.40.4/apis/middlewares_rate_limit.go", + "s1b04c2cd16ec": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L234-L239", + "sb63f49346abe": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L73-L79", + "sd700e81880f2": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L129-L139", + "s54430c83a38a": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/pocketbase.go#L165-L173", + "s2c8f78f2d5ac": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/tools/osutils/run.go#L10-L19", + "s5b0dd2fd7a6a": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/base.go#L1277-L1286", + "sf7c2fb5e78e7": "https://github.com/pocketbase/pocketbase/blob/v0.40.4/core/base.go#L1489-L1499" + } + }, + "appwrite": { + "name": "Appwrite", + "basis": "2.2.0", + "sources": { + "s9f5bd3b70f14": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/config/variables.php", + "sadb8c6e7b00a": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/.env", + "s2bb21218c5ee": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/api/account.php", + "s193bfa5c6e22": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/shared/api.php", + "s427bab3733b6": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/controllers/general.php", + "se9480e5807b3": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Create.php", + "sf916851a0b10": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php#L82-L84", + "s5b4b3eb2271f": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L129-L134", + "s93647e16b966": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L211-L212", + "sda698c40fc34": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php#L237-L287", + "s655a0abb9ef0": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php", + "s4ab95ecbf6b5": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/app/init/constants.php", + "s8e5272615f2e": "https://raw.githubusercontent.com/appwrite/appwrite/2.2.0/docker-compose.yml", + "s7ba6f9c71972": "https://github.com/appwrite/appwrite/blob/2.2.0/composer.lock#L3461-L3471", + "s5396e119c026": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php#L80-L95", + "s82988de645fa": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php#L103-L120", + "s224cc9d8a387": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Utopia/Database/Documents/User.php#L75-L109", + "sc55465cf878d": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L144-L161", + "sed1bf9453ecb": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L312-L339", + "s8784d02c82c9": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L357-L361", + "sf60a2b283b80": "https://github.com/appwrite/appwrite/blob/2.2.0/app/controllers/shared/api.php#L503-L507", + "s1fe0e6185518": "https://github.com/appwrite/appwrite/blob/2.2.0/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php#L221-L227" + } + }, + "pb-docs": { + "name": "PocketBase documentation (rolling)", + "basis": "unknown", + "sources": { + "s9029ab974cf1": "https://pocketbase.io/docs/going-to-production/", + "saedbdfbdfc48": "https://pocketbase.io/docs/api-rules-and-filters/", + "s773e441dd51c": "https://pocketbase.io/docs/authentication/", + "sf711b6345643": "https://pocketbase.io/docs/files-handling/", + "sb850a71fad4d": "https://pocketbase.io/docs/api-realtime/" + } + }, + "appwrite-docs": { + "name": "Appwrite documentation (rolling)", + "basis": "unknown", + "sources": { + "s4a965bdeeb98": "https://appwrite.io/docs/advanced/self-hosting/production/security", + "saa37bb5d4d7e": "https://appwrite.io/docs/partners/project/api-keys", + "sa0bd132f7968": "https://appwrite.io/docs/advanced/security/permissions", + "s58ad28816c5d": "https://appwrite.io/docs/products/databases/tablesdb/legacy/permissions", + "s2bf9a4746d28": "https://appwrite.io/docs/advanced/self-hosting/configuration/environment-variables", + "s000d05106450": "https://appwrite.io/docs/advanced/security/rate-limits", + "s38a1efa0c7d2": "https://appwrite.io/docs/advanced/self-hosting/configuration/email", + "s256ef73f7b85": "https://appwrite.io/docs/advanced/self-hosting/production/backups", + "s221c6aaef8df": "https://appwrite.io/docs/advanced/security/mfa" + } + }, + "utopia-php-database": { + "name": "utopia-php/database", + "basis": "7.3.9", + "sources": { + "s847e0ded0300": "https://github.com/utopia-php/database/blob/54364c687d6ddb3d976d7130b9001c0b29276933/src/Database/Helpers/Permission.php#L151-L155" + } + }, + "curl": { + "name": "curl", + "basis": "curl-8_14_1", + "sources": { + "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", + "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md", + "s82a34ee18632": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/config.md" + } + } + }, + "claims": { + "pb-bootstrap": {"text": "Privately bootstrap through the logged installer URL; superuser create EMAIL PASS exposes passwords in argv. Ordinary registration does not grant superuser rights.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:saf684a26056b"], "status": "REASONED"}, + "pb-installer": {"text": "Installer system-superuser token lasts about 30 minutes; protect its URL and startup logs as credentials.", "components": ["pb"], "sources": ["pb:saf684a26056b"], "status": "REASONED"}, + "pb-ips": {"text": "Superuser IP allowlists are available from v0.38.0; superuserIPs defaults empty and restricts authenticated requests, not /_/ static assets.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s5b66ca69bf6d"], "status": "REASONED"}, + "pb-tls": {"text": "serve with a domain provides native HTTPS/ACME; alternatively terminate TLS at a reverse proxy.", "components": ["pb"], "sources": ["pb:sb2129128b86c"], "status": "REASONED"}, + "pb-bind": {"text": "Without a domain serve defaults to 127.0.0.1:8090; with a domain it uses 0.0.0.0:80/443. Behind a local proxy omit the domain and set --http loopback.", "components": ["pb"], "sources": ["pb:s4b155ef30262"], "status": "REASONED"}, + "pb-origins": {"text": "--origins defaults * and accepts comma-separated allowed origins; CORS does not authorize records.", "components": ["pb"], "sources": ["pb:s4b155ef30262"], "status": "REASONED"}, + "pb-proxy": {"text": "Trust only headers the proxy overwrites, review useLeftmostIP ordering and block direct backend access to protect IP/rate controls.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a"], "status": "REASONED"}, + "pb-dev": {"text": "--dev adds diagnostics including SQL on stderr, not an auth bypass; its configured default comes from New(): true if os.Args[0] starts with a nonempty os.TempDir() or cacheDir() prefix, false otherwise.", "components": ["pb"], "sources": ["pb:seb56d3763223", "pb:s1b04c2cd16ec", "pb:sb63f49346abe", "pb:s2c8f78f2d5ac", "pb:sd700e81880f2", "pb:s5b0dd2fd7a6a", "pb:sf7c2fb5e78e7"], "status": "REASONED"}, + "pb-cli": {"text": "In the pinned CLI wrapper, Start() registers the serve and superuser commands on the root command and executes it.", "components": ["pb"], "sources": ["pb:s54430c83a38a"], "status": "REASONED"}, + "pb-rules": {"text": "Each list/view/create/update/delete rule defaults null (superuser-only); empty opens to guests and nonempty filters. Superusers bypass rules.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:saedbdfbdfc48", "pb:s04ae861b5709"], "status": "REASONED"}, + "pb-manage": {"text": "Top-level manageRule defaults null and permits privileged auth-record changes alongside create/update rules; its validator rejects empty-string rules.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-files": {"text": "File fields default unprotected despite locked record rules; Protected plus an authorized viewRule gates downloads, while a public viewRule still permits public access.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sf711b6345643", "pb:sa5e571f8d473"], "status": "REASONED"}, + "pb-file-token": {"text": "Short-lived file tokens provide identity context, not file authorization; viewRule decides. Protect token-bearing file URLs.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sf711b6345643", "pb:sa5e571f8d473"], "status": "REASONED"}, + "pb-realtime": {"text": "Collection subscriptions use listRule and individual-record subscriptions viewRule; SSE/subscription success does not prove event authorization.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sb850a71fad4d", "pb:sfd2f779e8e11"], "status": "REASONED"}, + "pb-password": {"text": "Auth initialization enables password authentication with email as the identity field.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-oauth": {"text": "OAuth2 defaults disabled and is unsupported for _superusers.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, + "pb-mfa-default": {"text": "MFA defaults disabled with duration 600 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-otp-default": {"text": "OTP defaults disabled with duration 180 seconds and length 8.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-alerts": {"text": "Authentication alerts default enabled; retain alerts and provide working email.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-auth-token": {"text": "Auth token duration defaults to 432000 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-file-duration": {"text": "File token duration defaults to 180 seconds.", "components": ["pb"], "sources": ["pb:sce114368c2ad"], "status": "REASONED"}, + "pb-auth-rule": {"text": "authRule defaults empty; verified-only collections can require verified = true after email verification works.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, + "pb-mfa": {"text": "For superusers retain password auth and enable both OTP and MFA; empty mfa.rule applies to everyone. OTP alone is not a second factor.", "components": ["pb", "pb-docs"], "sources": ["pb:sce114368c2ad", "pb-docs:s773e441dd51c"], "status": "REASONED"}, + "pb-logout": {"text": "Stateless authentication means clearing pb.authStore removes only the local copy, not a stolen token.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s773e441dd51c", "pb:s04ae861b5709"], "status": "REASONED"}, + "pb-encryption": {"text": "Settings default to plaintext JSON; select a 32-character environment secret with --encryptionEnv. This encrypts settings, not the whole database or files.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a"], "status": "REASONED"}, + "pb-backups": {"text": "Backups default local with empty cron (automatic backups off); archives contain main and logs databases plus remaining pb_data, excluding the backups directory and tracked exclusions; uploaded collection files on S3 require manual backup. Schedule/retain deliberately and protect archives.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a", "pb:se74188862a55"], "status": "REASONED"}, + "pb-backup-auth": {"text": "Backup downloads require a superuser file token and obey superuser IP restrictions; ordinary auth/file tokens do not suffice.", "components": ["pb"], "sources": ["pb:s31a5adccd695"], "status": "REASONED"}, + "pb-limiter": {"text": "Native limits exist from v0.23.0 but default disabled at v0.40.4; enable rateLimits.enabled.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:s9029ab974cf1", "pb:s03dd7dd84c2a"], "status": "REASONED"}, + "pb-limit-rules": {"text": "Seeded limits are *:auth 2/3s, *:create 20/5s, /api/batch 3/1s and /api/ 300/10s.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a"], "status": "REASONED"}, + "pb-limit-bypass": {"text": "Superusers and rateLimits.excludedIPs bypass limits; test ordinary non-excluded clients.", "components": ["pb"], "sources": ["pb:sf6ffdca4dd39"], "status": "REASONED"}, + "pb-smtp": {"text": "smtp.enabled and smtp.tls default false; disabled SMTP falls back to sendmail. Configure protected transport and test actual delivery.", "components": ["pb"], "sources": ["pb:s03dd7dd84c2a", "pb:seb56d3763223"], "status": "REASONED"}, + "appwrite-https": {"text": "API FORCE_HTTPS and function/site ROUTER_FORCE_HTTPS default disabled at 2.2.0; explicitly enable both despite conflicting rolling deprecation/default documentation.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s427bab3733b6", "appwrite-docs:s2bf9a4746d28"], "status": "REASONED"}, + "appwrite-hosts": {"text": "ROUTER_PROTECTION defaults disabled; configure actual domains and enable rejection of unknown hostnames.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s427bab3733b6"], "status": "REASONED"}, + "appwrite-registration": {"text": "Console root-only registration defaults enabled with empty email/IP allowlists; these restrict account creation, not dashboard reachability or login.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:s2bb21218c5ee"], "status": "REASONED"}, + "appwrite-console": {"text": "Claim the first operator privately and use a separate network/access boundary for a private dashboard.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s4a965bdeeb98", "appwrite:s2bb21218c5ee"], "status": "REASONED"}, + "appwrite-mfa": {"text": "Enroll and verify operator TOTP, protect recovery codes; at 2.2.0 enabled MFA with a verified factor requires a second session factor.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s221c6aaef8df", "appwrite:s193bfa5c6e22"], "status": "REASONED"}, + "appwrite-dev-env": {"text": "Development env disables root-only registration, abuse and router protection and carries placeholders; do not inherit it for production.", "components": ["appwrite"], "sources": ["appwrite:sadb8c6e7b00a"], "status": "REASONED"}, + "appwrite-recreate": {"text": "Apply env/Compose changes with docker compose up -d from the install directory, then check effective behavior.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s4a965bdeeb98"], "status": "REASONED"}, + "appwrite-permissions": {"text": "Collection and document grants are additive according to legacy documentation; documentSecurity defaults false in Appwrite 2.2.0 and must be enabled for document grants. The pinned parameter description states the need for one permission level.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:sf916851a0b10", "appwrite-docs:s58ad28816c5d"], "status": "REASONED"}, + "appwrite-creation": {"text": "For null or omitted document permissions, the pinned aggregate helper preserves null before the handler assigns read/update/delete user grants to a nonprivileged requester with a user ID, or an empty list otherwise. API-key classification skips role validation but does not itself determine the default list. Use specific users/teams for private data.", "components": ["appwrite", "appwrite-docs", "utopia-php-database"], "sources": ["appwrite:s5b4b3eb2271f", "appwrite:s93647e16b966", "appwrite:sda698c40fc34", "appwrite-docs:sa0bd132f7968", "appwrite:s7ba6f9c71972", "utopia-php-database:s847e0ded0300"], "status": "REASONED"}, + "appwrite-read-path": {"text": "Document GET classifies the requester from authorization roles (keys; owner/developer/admin privileged), reports disabled databases/collections as not found except to those requesters, and fetches the document through the database layer; collection permissions and documentSecurity are passed to the database layer at collection creation.", "components": ["appwrite"], "sources": ["appwrite:s5396e119c026", "appwrite:s82988de645fa", "appwrite:s224cc9d8a387", "appwrite:s1fe0e6185518"], "status": "REASONED"}, + "appwrite-requester": {"text": "With an API key the request role and scopes come from the key; a standard or ephemeral project key for the same project disables the default authorization status, as do admin-mode console sessions outside console project routes, while route scope validation applies to every requester.", "components": ["appwrite"], "sources": ["appwrite:sc55465cf878d", "appwrite:sed1bf9453ecb", "appwrite:s8784d02c82c9", "appwrite:sf60a2b283b80"], "status": "REASONED"}, + "appwrite-keys": {"text": "Properly scoped server API keys bypass resource permissions but still obey operation scopes; keys.write is admin-equivalent and keys never belong in clients.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s193bfa5c6e22", "appwrite-docs:saa37bb5d4d7e"], "status": "REASONED"}, + "appwrite-encryption": {"text": "Replace _APP_OPENSSL_KEY_V1=your-secret-key before production data; preserve the exact key separately, since replacement/loss strands encrypted secrets.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, + "appwrite-backups": {"text": "Back up database, persistent storage and deployment configuration; restrict access and test restoration in a separate installation.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, + "appwrite-abuse": {"text": "_APP_OPTIONS_ABUSE defaults enabled but dev env disables it; server API-key requests are exempt, so test with ordinary clients.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite:sadb8c6e7b00a", "appwrite:s193bfa5c6e22", "appwrite-docs:s000d05106450"], "status": "REASONED"}, + "appwrite-smtp": {"text": "SMTP host/port/secure/username/password default empty and empty host disables sending; configure the provider and confirm message receipt/use.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s9f5bd3b70f14", "appwrite-docs:s38a1efa0c7d2"], "status": "REASONED"}, + "appwrite-storage": {"text": "Storage defaults local, upload limit 30000000 and server antivirus disabled; enabling scanning requires configured reachable ClamAV.", "components": ["appwrite"], "sources": ["appwrite:s9f5bd3b70f14"], "status": "REASONED"}, + "appwrite-bucket": {"text": "New bucket permissions are empty and fileSecurity=false; enable per-file security deliberately, since bucket grants also permit access.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3"], "status": "REASONED"}, + "appwrite-bucket-options": {"text": "Bucket encryption and antivirus default true; bucket antivirus does not enable the server scanner.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3"], "status": "REASONED"}, + "appwrite-size": {"text": "Encryption/scanning skip files above 20000000 bytes; cap maximumFileSize at that threshold when either is mandatory and test the boundary.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3", "appwrite:s4ab95ecbf6b5"], "status": "REASONED"}, + "appwrite-functions": {"text": "Function execute roles and execution-key scopes default empty; grant narrowly. Disabled functions still admit authorized Server SDK API keys.", "components": ["appwrite"], "sources": ["appwrite:s655a0abb9ef0"], "status": "REASONED"}, + "appwrite-executor": {"text": "Keep executor/orchestrator private, protect Docker-socket authority and replace _APP_EXECUTOR_SECRET; container execution is not demonstrated hostile-tenant isolation.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite:s8e5272615f2e", "appwrite-docs:s2bf9a4746d28"], "status": "REASONED"}, + "verify-pb-records": {"text": "Public rules allow guest reads; locked rules deny with 403, unsatisfied list filters can return empty 200 and view rules 404. Retain owner and separate superuser controls.", "components": ["pb-docs", "curl"], "sources": ["pb-docs:saedbdfbdfc48", "curl:s5e1383feef7d", "curl:s3db522258e44"], "status": "REASONED", "verify": [1]}, + "verify-appwrite-records": {"text": "Broad collection grants defeat document restrictions; removing them must deny unrelated users while document grantees succeed. Compare client/server creation defaults.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s58ad28816c5d", "appwrite-docs:sa0bd132f7968"], "status": "REASONED", "verify": [1]}, + "verify-appwrite-keys": {"text": "A correctly scoped server key reads despite empty permissions, while a key without scope fails; neither substitutes for an ordinary-user control.", "components": ["appwrite"], "sources": ["appwrite:s193bfa5c6e22"], "status": "REASONED", "verify": [1]}, + "verify-registration": {"text": "On disposable Appwrite fixtures compare open signup 201 with root-only account-limit failure and independent email/IP restrictions; retain login/invitation controls.", "components": ["appwrite"], "sources": ["appwrite:s2bb21218c5ee"], "status": "REASONED", "verify": [2]}, + "verify-pb-bootstrap": {"text": "Guest and ordinary-user _superusers record creation must fail while a valid installer token creates the private operator; static dashboard assets prove no admin access.", "components": ["pb"], "sources": ["pb:saf684a26056b", "pb:s69aa223f52e4"], "status": "REASONED"}, + "verify-appwrite-mfa": {"text": "Console account GET succeeds without MFA, fails with user_more_factors_required for a fresh password-only MFA session, and succeeds after TOTP completion.", "components": ["appwrite", "appwrite-docs"], "sources": ["appwrite-docs:s221c6aaef8df", "appwrite:s193bfa5c6e22"], "status": "REASONED"}, + "verify-pb-writes": {"text": "Compare intended/unauthorized POST/PATCH/DELETE and privileged auth-record management; locked manageRule grants no management and empty-string configuration must fail.", "components": ["pb"], "sources": ["pb:s69aa223f52e4", "pb:sce114368c2ad"], "status": "REASONED"}, + "verify-pb-files": {"text": "Before protection unsigned file GET works; Protected plus private viewRule rejects guests/unrelated-user file tokens with 404 while permitted-user bytes still arrive.", "components": ["pb", "curl"], "sources": ["pb:sa5e571f8d473", "curl:s82a34ee18632"], "status": "REASONED"}, + "verify-pb-realtime": {"text": "Mutate a fixture while comparing collection and record subscriptions; deny unauthorized event delivery, retaining authorized delivery. PB_CONNECT/acknowledgment alone is insufficient.", "components": ["pb", "pb-docs"], "sources": ["pb-docs:sb850a71fad4d", "pb:sfd2f779e8e11"], "status": "REASONED"}, + "verify-pb-mfa": {"text": "Compare password-only completion with password-plus-OTP MFA and verified/unverified authRule logins; clearing authStore must not revoke a retained valid token.", "components": ["pb-docs"], "sources": ["pb-docs:s773e441dd51c"], "status": "REASONED"}, + "verify-pb-ips": {"text": "Compare allowed/excluded superuser sources and forged forwarded headers; excluded sources must get 403 and direct backend access must fail with a working proxy control.", "components": ["pb"], "sources": ["pb:s5b66ca69bf6d", "pb:s03dd7dd84c2a"], "status": "REASONED"}, + "verify-transport": {"text": "Compare HTTP/HTTPS and certificates, Appwrite API/function/site domains and configured/unknown Host; compare browser origins separately from record authorization.", "components": ["pb", "appwrite"], "sources": ["pb:s4b155ef30262", "appwrite:s427bab3733b6"], "status": "REASONED"}, + "verify-abuse": {"text": "Cross each configured threshold with ordinary non-excluded clients and compare disabled/enabled controls; PocketBase limiting yields 429 with later successful recovery.", "components": ["pb", "appwrite-docs"], "sources": ["pb:sf6ffdca4dd39", "appwrite-docs:s000d05106450"], "status": "REASONED"}, + "verify-email": {"text": "Request OTP, verification and recovery messages and confirm receipt and use; request success alone is not delivery evidence.", "components": ["pb", "appwrite-docs"], "sources": ["pb:seb56d3763223", "appwrite-docs:s38a1efa0c7d2"], "status": "REASONED"}, + "verify-pb-recovery": {"text": "Compare plaintext/encrypted persisted settings, restart with retained key and restore local/S3 data; only permitted superuser file tokens may download backups.", "components": ["pb", "pb-docs"], "sources": ["pb:s03dd7dd84c2a", "pb:s31a5adccd695", "pb-docs:s9029ab974cf1"], "status": "REASONED"}, + "verify-appwrite-recovery": {"text": "Restore encrypted fixtures with the original key; a separate wrong-key fixture must not recover encrypted values.", "components": ["appwrite-docs"], "sources": ["appwrite-docs:s256ef73f7b85"], "status": "REASONED"}, + "verify-uploads": {"text": "Compare permitted/unrelated uploads and size-boundary fixtures; mandatory processing requires rejecting over-limit files and confirming accepted-file encryption/scanning.", "components": ["appwrite"], "sources": ["appwrite:se9480e5807b3", "appwrite:s4ab95ecbf6b5"], "status": "REASONED"}, + "verify-functions": {"text": "Compare intended execute roles, unrelated users and privileged server keys, including disabled functions; confirm executor/orchestrator privacy from a second host.", "components": ["appwrite"], "sources": ["appwrite:s655a0abb9ef0", "appwrite:s8e5272615f2e"], "status": "REASONED"}, + "verify-bundle": {"text": "Search filenames using protected exact-secret patterns with a disposable marker control; client bundles must contain neither superuser tokens nor Appwrite API keys, and encoded/split forms can evade scanning.", "components": ["pb-docs", "appwrite-docs"], "sources": ["pb-docs:s773e441dd51c", "appwrite-docs:saa37bb5d4d7e"], "status": "REASONED", "verify": [3]}, + "local-guards": {"text": "Recorded guard tests rejected placeholders and omitted/shortened arguments; this demonstrates shell behavior only, not service access or deployed-bundle scanning.", "components": ["pb-docs", "appwrite-docs"], "sources": ["pb-docs:saedbdfbdfc48", "appwrite-docs:sa0bd132f7968", "appwrite-docs:saa37bb5d4d7e"], "status": "DEMONSTRATED", "evidence": "Local guard tests refused embedded `REPLACE_WITH_` placeholders, `example.com`, angle brackets, empty arguments, and omitted or shortened `set --` lines."}, + "local-syntax": {"text": "All three bash fences have recorded local lint/syntax success, not live service evidence; ShellCheck version is recorded only in the body.", "components": ["appwrite", "pb-docs", "appwrite-docs"], "sources": ["pb-docs:saedbdfbdfc48", "appwrite:s2bb21218c5ee", "appwrite-docs:saa37bb5d4d7e"], "status": "DEMONSTRATED", "evidence": "All three bash blocks passed ShellCheck 0.11.0 and `bash -n` during authoring."}, + "source-limits": {"text": "The PocketBase CLI wrapper, --dev default, and Appwrite requester classification and document-read call sites are now cited; how utopia-php/database matches roles against collection/document permissions (including documentSecurity) and what the disabled default authorization status bypasses remain untraced in that unfetched dependency.", "components": ["appwrite-docs", "pb-docs", "appwrite"], "sources": ["appwrite-docs:s58ad28816c5d", "pb-docs:s9029ab974cf1", "appwrite:s7ba6f9c71972"], "status": "REASONED"} + } +} +```` + + ====================================================================== ==> headless-browser-services.md ====================================================================== diff --git a/tools/test_version_basis.py b/tools/test_version_basis.py index c63820f56..e66d546d4 100644 --- a/tools/test_version_basis.py +++ b/tools/test_version_basis.py @@ -1776,6 +1776,35 @@ def components(pairs): with self.assertRaisesRegex(ValueError, '400-character header limit'): components([('x' * 400, 'v1')]) + def test_header_count_grammar(self): + for count, noun, verb, carry in ((1, 'source', 'is', 'carries'), + (2, 'sources', 'are', 'carry')): + with self.subTest(count=count): + data = fixture() + data['components'] = { + f'c{i}': dict(name=f'Product{i}', basis='v1', sources={SID: URL}) + for i in range(8 + count)} + basis = 'basis' if count == 1 else 'bases' + self.assertIn(f', plus {count} more recorded {basis}.', vb.header(data)) + for pinned, unknown in ((count, 0), (0, count), (count, count)): + with self.subTest(pinned=pinned, unknown=unknown): + data = fixture() + for i in range(pinned): + data['components'][f'p{i}'] = dict( + name=f'Pin{i}', basis='a' * 7, sources={SID: URL}) + for i in range(unknown): + data['components'][f'u{i}'] = dict( + name=f'Docs{i}', basis='unknown', sources={SID: URL}) + clauses = [] + if pinned: + clauses.append(f'{count} further {noun} {verb} pinned to a commit') + if unknown: + subject = str(count) if pinned else f'{count} further {noun}' + clauses.append(f'{subject} {carry} no recorded version') + expected = 'Written against Product v1.0; ' + ' and '.join(clauses) + '.' + self.assertEqual(vb.versions_sentence(data), expected) + self.assertTrue(vb.header(data).startswith(expected + ' Metadata reviewed')) + def test_commit_pinned_headline(self): data = fixture() for size in (7, 40): @@ -1840,7 +1869,7 @@ def test_headline_limits(self): self.assertEqual(vb.versions_sentence(data), 'Written against Headline' + suffix + ', ' + ', '.join(f'Other{i} v1' for i in range(6)) + - ', and Other6 v1, plus 1 more recorded bases.') + ', and Other6 v1, plus 1 more recorded basis.') # Exact boundary includes the truncation and folded-count clauses. data['components']['docs'] = dict(name='Docs', basis='unknown', sources={SID: URL}) tail = ', plus 8 more recorded bases; 1 further source carries no recorded version.' diff --git a/tools/version_basis.py b/tools/version_basis.py index e784ef5d0..c613ac301 100644 --- a/tools/version_basis.py +++ b/tools/version_basis.py @@ -894,7 +894,8 @@ def versions_sentence(data): for keep in range(min(8, len(entries)), 0, -1): sentence = 'Written against ' + joined(entries[:keep]) if keep < len(entries): - sentence += f', plus {len(entries) - keep} more recorded bases' + omitted = len(entries) - keep + sentence += f', plus {omitted} more recorded ' + ('basis' if omitted == 1 else 'bases') if folded: sentence += '; ' + ' and '.join(folded) sentence += '.' diff --git a/traefik.md b/traefik.md index 481d64dea..71db7b10a 100644 --- a/traefik.md +++ b/traefik.md @@ -1,188 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-05", - "documentation_checked": "2026-09", - "body_sha256": "aac661357effc89cf9b208294d0f255b4fb31c03428609d9756c4fa69347809c", - "components": { - "traefik-docs": { - "name": "Traefik documentation (rolling)", - "basis": "unknown", - "sources": { - "scbc0f2135c78": "https://doc.traefik.io/traefik/" - } - }, - "docker": { - "name": "Traefik Docker provider", - "basis": "unknown", - "sources": { - "se23fe4fe79fa": "https://doc.traefik.io/traefik/reference/install-configuration/providers/docker/" - } - }, - "swarm": { - "name": "Traefik Swarm provider", - "basis": "unknown", - "sources": { - "s256d46f3f558": "https://doc.traefik.io/traefik/reference/install-configuration/providers/swarm/" - } - }, - "middleware": { - "name": "Traefik middleware documentation", - "basis": "unknown", - "sources": { - "sd987195d64dd": "https://doc.traefik.io/traefik/middlewares/http/buffering/", - "sd20dcba7ebca": "https://doc.traefik.io/traefik/middlewares/http/inflightreq/", - "scf1b1e35ef5d": "https://doc.traefik.io/traefik/middlewares/http/ratelimit/" - } - }, - "curl": { - "name": "curl write-out minimum", - "basis": "7.75.0", - "sources": { - "s2b2686afaf41": "https://curl.se/docs/manpage.html" - } - }, - "traefik-v3": { - "name": "Traefik v3 documentation", - "basis": "v3.5", - "sources": { - "sd6ad276040b7": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/entrypoints/", - "s418a6b1ad09b": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/tls/certificate-resolvers/acme/", - "s25d849b2d428": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/tls/tls-options/", - "s3acb3f5b1778": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/providers/others/file/", - "s5d7a6940f1dd": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/middlewares/basicauth/", - "s3cf7d994a56a": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/middlewares/forwardauth/" - } - }, - "traefik-source": { - "name": "Traefik ACME storage source", - "basis": "v3.5.0", - "sources": { - "s0e08b0bdcb35": "https://github.com/traefik/traefik/blob/v3.5.0/pkg/provider/acme/local_store.go#L125" - } - }, - "httpd-source": { - "name": "Apache httpd htpasswd source", - "basis": "2.4.69", - "sources": { - "sf711558bdf3c": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L45", - "s4aa0f7f341fb": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L292-L295", - "s4482c55d79a2": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L382", - "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128" - } - }, - "owasp-source": { - "name": "OWASP Password Storage Cheat Sheet", - "basis": "72865b1e3b5754246a101778dc00b37f9c233373", - "sources": { - "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" - } - }, - "cloudflare-source": { - "name": "Cloudflare Access MFA documentation", - "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", - "sources": { - "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" - } - }, - "traefik-basicauth-source": { - "name": "Traefik BasicAuth source", - "basis": "v3.5.0", - "sources": { - "s5f9a13d720c8": "https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/basic_auth.go#L67-L99" - } - } - }, - "claims": { - "entrypoints": {"text": "For Traefik v2/v3, web :80 redirects to HTTPS websecure :443 and an ACME resolver obtains/renews certificates.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:sd6ad276040b7", "traefik-v3:s418a6b1ad09b"], "status": "REASONED"}, - "acme-storage": {"text": "Persist acme.json across restarts with mode 600 to retain certificates and avoid repeated issuance/rate limits.", "components": ["traefik-docs", "traefik-v3", "traefik-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s418a6b1ad09b", "traefik-source:s0e08b0bdcb35"], "status": "REASONED"}, - "acme-challenges": {"text": "TLS-ALPN requires inbound 443; HTTP-01 uses 80, while DNS supports wildcards without inbound ports.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s418a6b1ad09b"], "status": "REASONED"}, - "docker-default": {"text": "Docker discovery defaults exposedByDefault to true; set false and opt each intended container in with traefik.enable=true.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, - "docker-scope": {"text": "Discovery is daemon-wide, not Compose-project scoped; eligible unlabelled containers get routers unless excluded. Missing usable ports skip service creation.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, - "docker-socket": {"text": "The Docker socket gives daemon control despite :ro; use a restricted socket proxy to limit API access.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, - "tls-floor": {"text": "Dynamic default TLS options set minVersion VersionTLS12.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s25d849b2d428"], "status": "REASONED"}, - "route": {"text": "Docker labels select the app host, websecure, letsencrypt resolver and backend port 3000.", "components": ["traefik-docs", "docker"], "sources": ["traefik-docs:scbc0f2135c78", "docker:se23fe4fe79fa"], "status": "REASONED"}, - "backend-isolation": {"text": "Publish only Traefik 80/443, not the app's 3000, to prevent middleware/TLS bypass.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, - "basic": {"text": "Attach bcrypt basicAuth to the router; Apache httpd 2.4.69 htpasswd -nB defaults to bcrypt cost 5, while -C 12 selects cost 12; OWASP at 72865b1e recommends 10 or more for legacy bcrypt.", "components": ["traefik-docs", "traefik-v3", "httpd-source", "owasp-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s5d7a6940f1dd", "httpd-source:sf711558bdf3c", "httpd-source:s4aa0f7f341fb", "httpd-source:s4482c55d79a2", "httpd-source:s309ecdd34379", "owasp-source:s4531e65b6502"], "status": "REASONED"}, - "compose-hash": {"text": "Double each dollar sign in Compose hash labels; file-provider hashes need no such escaping.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s5d7a6940f1dd"], "status": "REASONED"}, - "file-provider": {"text": "Load dynamic files with providers.file.directory/filename and reference their middleware from Docker as app-auth@file.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s3acb3f5b1778"], "status": "REASONED"}, - "mfa": {"text": "Basic is single-factor; use forwardAuth with Authelia/oauth2-proxy or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization).", "components": ["traefik-docs", "traefik-v3", "cloudflare-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s3cf7d994a56a", "cloudflare-source:sdcac732541da"], "status": "REASONED"}, - "body-limit": {"text": "Attach buffering.maxRequestBodyBytes=10485760 alongside existing auth/routing labels; it bounds accepted body size.", "components": ["middleware"], "sources": ["middleware:sd987195d64dd"], "status": "REASONED"}, - "buffer-storage": {"text": "memRequestBodyBytes defaults to 1048576 and controls the memory-to-disk threshold independently of maximum accepted body size.", "components": ["middleware"], "sources": ["middleware:sd987195d64dd"], "status": "REASONED"}, - "rate-limit": {"text": "average=10 and burst=20 configure a rate cap, per source by default.", "components": ["middleware"], "sources": ["middleware:scf1b1e35ef5d"], "status": "REASONED"}, - "inflight": {"text": "inflightreq.amount=10 bounds concurrent requests; sourceCriterion defaults to request host rather than client, so configure a per-client cap explicitly.", "components": ["middleware"], "sources": ["middleware:sd20dcba7ebca"], "status": "REASONED"}, - "middleware-order": {"text": "Rate/inflight precede auth to count rejected logins: Traefik v3.5.0 BasicAuth returns on failure without calling the next handler; buffering follows admission controls to avoid buffering rejected uploads.", "components": ["traefik-docs", "middleware", "traefik-basicauth-source"], "sources": ["traefik-docs:scbc0f2135c78", "middleware:sd987195d64dd", "middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca", "traefik-basicauth-source:s5f9a13d720c8"], "status": "REASONED"}, - "dashboard": {"text": "Keep api.insecure and unprotected api@internal routers off public entry points or protect the dashboard with auth.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED"}, - "swarm": {"text": "Standalone Docker guidance does not cover Swarm; v3 uses providers.swarm and its separate exposedByDefault also defaults true.", "components": ["swarm"], "sources": ["swarm:s256d46f3f558"], "status": "REASONED"}, - "verify-redirect": {"text": "HTTP should redirect to HTTPS.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, - "verify-auth": {"text": "Unauthenticated HTTPS should return 401 once auth is attached.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, - "verify-size": {"text": "Authenticated 1M must reach the app and 11M return 413; 401/000 invalidate the control. Remove app-body in isolation to rule out backend refusal.", "components": ["middleware", "traefik-docs"], "sources": ["middleware:sd987195d64dd", "traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, - "observed-429": {"text": "The guide records only a 429 response; no limiter attribution or exposed/fixed demonstration is established.", "components": ["middleware"], "sources": ["middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca"], "status": "DEMONSTRATED", "evidence": "A 429 appeared. That is all this shows."}, - "verify-rate": {"text": "Concurrent unauthenticated probes can reach pre-auth limiters; 429 alone cannot distinguish rate, inflight or upstream refusal. Compare each limiter alone against both disabled.", "components": ["middleware", "traefik-basicauth-source"], "sources": ["middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca", "traefik-basicauth-source:s5f9a13d720c8"], "status": "REASONED", "verify": [1]}, - "verify-isolation": {"text": "Compose ps covers one project; ss misses NAT-only publication without a userland proxy. Neither finds unintended routes through Traefik.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED", "verify": [1]}, - "verify-acme": {"text": "Inspect ACME startup errors; failed issuance can leave a self-signed TRAEFIK DEFAULT CERT.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, - "canary-route": {"text": "On a disposable deployment, unlabelled traefik/whoami has one HTTP port; use its generated Host rule and plain HTTP even on :443 because the generated router requests no TLS.", "components": ["docker", "traefik-docs"], "sources": ["docker:se23fe4fe79fa", "traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [2]}, - "verify-canary": {"text": "Calibrate exposedByDefault true with 200 and the canary Hostname body; false should give 404. Missing discovery/provider also gives 404, unreachable backend 502/504.", "components": ["docker", "curl"], "sources": ["docker:se23fe4fe79fa", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]}, - "verify-app-control": {"text": "In the same run, the app must return 401 or known authenticated 200 while the canary gives 404; app 401 proves routing, not proxy-auth attachment. Keep TLS verification.", "components": ["docker", "traefik-docs", "curl"], "sources": ["docker:se23fe4fe79fa", "traefik-docs:scbc0f2135c78", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [3]} - } -} ---- # Traefik: automatic TLS and authentication middleware - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-05; documentation checked 2026-09 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- traefik-docs: Traefik documentation (rolling) (unknown) -- docker: Traefik Docker provider (unknown) -- swarm: Traefik Swarm provider (unknown) -- middleware: Traefik middleware documentation (unknown) -- curl: curl write-out minimum (7.75.0) -- traefik-v3: Traefik v3 documentation (v3.5) -- traefik-source: Traefik ACME storage source (v3.5.0) -- httpd-source: Apache httpd htpasswd source (2.4.69) -- owasp-source: OWASP Password Storage Cheat Sheet (72865b1e3b5754246a101778dc00b37f9c233373) -- cloudflare-source: Cloudflare Access MFA documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- traefik-basicauth-source: Traefik BasicAuth source (v3.5.0) - -Claims (full source mapping in the front matter): +Written against Traefik v3 documentation v3.5, Traefik ACME storage source v3.5.0, Traefik BasicAuth source v3.5.0, Apache httpd htpasswd source 2.4.69, and curl write-out minimum 7.75.0; 2 further sources are pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-10-05; documentation checked September 2026. The guide records 30 claims: 1 demonstrated by a check that ran and 29 reasoned from cited sources. -- entrypoints (REASONED; traefik-docs, traefik-v3): For Traefik v2/v3, web :80 redirects to HTTPS websecure :443 and an ACME resolver obtains/renews certificates. -- acme-storage (REASONED; traefik-docs, traefik-v3, traefik-source): Persist acme.json across restarts with mode 600 to retain certificates and avoid repeated issuance/rate limits. -- acme-challenges (REASONED; traefik-docs, traefik-v3): TLS-ALPN requires inbound 443; HTTP-01 uses 80, while DNS supports wildcards without inbound ports. -- docker-default (REASONED; docker): Docker discovery defaults exposedByDefault to true; set false and opt each intended container in with traefik.enable=true. -- docker-scope (REASONED; docker): Discovery is daemon-wide, not Compose-project scoped; eligible unlabelled containers get routers unless excluded. Missing usable ports skip service creation. -- docker-socket (REASONED; docker): The Docker socket gives daemon control despite :ro; use a restricted socket proxy to limit API access. -- tls-floor (REASONED; traefik-docs, traefik-v3): Dynamic default TLS options set minVersion VersionTLS12. -- route (REASONED; traefik-docs, docker): Docker labels select the app host, websecure, letsencrypt resolver and backend port 3000. -- backend-isolation (REASONED; docker): Publish only Traefik 80/443, not the app's 3000, to prevent middleware/TLS bypass. -- basic (REASONED; traefik-docs, traefik-v3, httpd-source, owasp-source): Attach bcrypt basicAuth to the router; Apache httpd 2.4.69 htpasswd -nB defaults to bcrypt cost 5, while -C 12 selects cost 12; OWASP at 72865b1e recommends 10 or more for legacy bcrypt. -- compose-hash (REASONED; traefik-docs, traefik-v3): Double each dollar sign in Compose hash labels; file-provider hashes need no such escaping. -- file-provider (REASONED; traefik-docs, traefik-v3): Load dynamic files with providers.file.directory/filename and reference their middleware from Docker as app-auth@file. -- mfa (REASONED; traefik-docs, traefik-v3, cloudflare-source): Basic is single-factor; use forwardAuth with Authelia/oauth2-proxy or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization). -- body-limit (REASONED; middleware): Attach buffering.maxRequestBodyBytes=10485760 alongside existing auth/routing labels; it bounds accepted body size. -- buffer-storage (REASONED; middleware): memRequestBodyBytes defaults to 1048576 and controls the memory-to-disk threshold independently of maximum accepted body size. -- rate-limit (REASONED; middleware): average=10 and burst=20 configure a rate cap, per source by default. -- inflight (REASONED; middleware): inflightreq.amount=10 bounds concurrent requests; sourceCriterion defaults to request host rather than client, so configure a per-client cap explicitly. -- middleware-order (REASONED; traefik-docs, middleware, traefik-basicauth-source): Rate/inflight precede auth to count rejected logins: Traefik v3.5.0 BasicAuth returns on failure without calling the next handler; buffering follows admission controls to avoid buffering rejected uploads. -- dashboard (REASONED; traefik-docs): Keep api.insecure and unprotected api@internal routers off public entry points or protect the dashboard with auth. -- swarm (REASONED; swarm): Standalone Docker guidance does not cover Swarm; v3 uses providers.swarm and its separate exposedByDefault also defaults true. -- verify-redirect (REASONED; traefik-docs): HTTP should redirect to HTTPS. -- verify-auth (REASONED; traefik-docs): Unauthenticated HTTPS should return 401 once auth is attached. -- verify-size (REASONED; middleware, traefik-docs): Authenticated 1M must reach the app and 11M return 413; 401/000 invalidate the control. Remove app-body in isolation to rule out backend refusal. -- observed-429 (DEMONSTRATED; middleware): The guide records only a 429 response; no limiter attribution or exposed/fixed demonstration is established. -- verify-rate (REASONED; middleware, traefik-basicauth-source): Concurrent unauthenticated probes can reach pre-auth limiters; 429 alone cannot distinguish rate, inflight or upstream refusal. Compare each limiter alone against both disabled. -- verify-isolation (REASONED; docker): Compose ps covers one project; ss misses NAT-only publication without a userland proxy. Neither finds unintended routes through Traefik. -- verify-acme (REASONED; traefik-docs): Inspect ACME startup errors; failed issuance can leave a self-signed TRAEFIK DEFAULT CERT. -- canary-route (REASONED; docker, traefik-docs): On a disposable deployment, unlabelled traefik/whoami has one HTTP port; use its generated Host rule and plain HTTP even on :443 because the generated router requests no TLS. -- verify-canary (REASONED; docker, curl): Calibrate exposedByDefault true with 200 and the canary Hostname body; false should give 404. Missing discovery/provider also gives 404, unreachable backend 502/504. -- verify-app-control (REASONED; docker, traefik-docs, curl): In the same run, the app must return 401 or known authenticated 200 while the canary gives 404; app 401 proves routing, not proxy-auth attachment. Keep TLS verification. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. Applies to Traefik v2 and v3. Traefik obtains and renews certificates itself through ACME resolvers, which suits container deployments. @@ -433,3 +253,209 @@ With the authentication from section 3 in place, this unauthenticated probe retu - Apache httpd 2.4.69 htpasswd usage (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128 - OWASP Password Storage Cheat Sheet at 72865b1e3b5754246a101778dc00b37f9c233373, legacy bcrypt work factor (checked October 2026): https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13 - Cloudflare Access MFA documentation at c896795b8e8bab68d05ae78d64c9cbcda1fdb104, application or policy MFA (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81 + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/traefik.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- traefik-v3: Traefik v3 documentation (v3.5) +- traefik-source: Traefik ACME storage source (v3.5.0) +- traefik-basicauth-source: Traefik BasicAuth source (v3.5.0) +- traefik-docs: Traefik documentation (rolling) (unknown) +- docker: Traefik Docker provider (unknown) +- swarm: Traefik Swarm provider (unknown) +- middleware: Traefik middleware documentation (unknown) +- httpd-source: Apache httpd htpasswd source (2.4.69) +- cloudflare-source: Cloudflare Access MFA documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- curl: curl write-out minimum (7.75.0) +- owasp-source: OWASP Password Storage Cheat Sheet (72865b1e3b5754246a101778dc00b37f9c233373) + +Claims (full source mapping in the JSON block below): + +- entrypoints (REASONED; traefik-docs, traefik-v3): For Traefik v2/v3, web :80 redirects to HTTPS websecure :443 and an ACME resolver obtains/renews certificates. +- acme-storage (REASONED; traefik-docs, traefik-v3, traefik-source): Persist acme.json across restarts with mode 600 to retain certificates and avoid repeated issuance/rate limits. +- acme-challenges (REASONED; traefik-docs, traefik-v3): TLS-ALPN requires inbound 443; HTTP-01 uses 80, while DNS supports wildcards without inbound ports. +- docker-default (REASONED; docker): Docker discovery defaults exposedByDefault to true; set false and opt each intended container in with traefik.enable=true. +- docker-scope (REASONED; docker): Discovery is daemon-wide, not Compose-project scoped; eligible unlabelled containers get routers unless excluded. Missing usable ports skip service creation. +- docker-socket (REASONED; docker): The Docker socket gives daemon control despite :ro; use a restricted socket proxy to limit API access. +- tls-floor (REASONED; traefik-docs, traefik-v3): Dynamic default TLS options set minVersion VersionTLS12. +- route (REASONED; traefik-docs, docker): Docker labels select the app host, websecure, letsencrypt resolver and backend port 3000. +- backend-isolation (REASONED; docker): Publish only Traefik 80/443, not the app's 3000, to prevent middleware/TLS bypass. +- basic (REASONED; traefik-docs, traefik-v3, httpd-source, owasp-source): Attach bcrypt basicAuth to the router; Apache httpd 2.4.69 htpasswd -nB defaults to bcrypt cost 5, while -C 12 selects cost 12; OWASP at 72865b1e recommends 10 or more for legacy bcrypt. +- compose-hash (REASONED; traefik-docs, traefik-v3): Double each dollar sign in Compose hash labels; file-provider hashes need no such escaping. +- file-provider (REASONED; traefik-docs, traefik-v3): Load dynamic files with providers.file.directory/filename and reference their middleware from Docker as app-auth@file. +- mfa (REASONED; traefik-docs, traefik-v3, cloudflare-source): Basic is single-factor; use forwardAuth with Authelia/oauth2-proxy or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization). +- body-limit (REASONED; middleware): Attach buffering.maxRequestBodyBytes=10485760 alongside existing auth/routing labels; it bounds accepted body size. +- buffer-storage (REASONED; middleware): memRequestBodyBytes defaults to 1048576 and controls the memory-to-disk threshold independently of maximum accepted body size. +- rate-limit (REASONED; middleware): average=10 and burst=20 configure a rate cap, per source by default. +- inflight (REASONED; middleware): inflightreq.amount=10 bounds concurrent requests; sourceCriterion defaults to request host rather than client, so configure a per-client cap explicitly. +- middleware-order (REASONED; traefik-docs, middleware, traefik-basicauth-source): Rate/inflight precede auth to count rejected logins: Traefik v3.5.0 BasicAuth returns on failure without calling the next handler; buffering follows admission controls to avoid buffering rejected uploads. +- dashboard (REASONED; traefik-docs): Keep api.insecure and unprotected api@internal routers off public entry points or protect the dashboard with auth. +- swarm (REASONED; swarm): Standalone Docker guidance does not cover Swarm; v3 uses providers.swarm and its separate exposedByDefault also defaults true. +- verify-redirect (REASONED; traefik-docs): HTTP should redirect to HTTPS. +- verify-auth (REASONED; traefik-docs): Unauthenticated HTTPS should return 401 once auth is attached. +- verify-size (REASONED; middleware, traefik-docs): Authenticated 1M must reach the app and 11M return 413; 401/000 invalidate the control. Remove app-body in isolation to rule out backend refusal. +- observed-429 (DEMONSTRATED; middleware): The guide records only a 429 response; no limiter attribution or exposed/fixed demonstration is established. +- verify-rate (REASONED; middleware, traefik-basicauth-source): Concurrent unauthenticated probes can reach pre-auth limiters; 429 alone cannot distinguish rate, inflight or upstream refusal. Compare each limiter alone against both disabled. +- verify-isolation (REASONED; docker): Compose ps covers one project; ss misses NAT-only publication without a userland proxy. Neither finds unintended routes through Traefik. +- verify-acme (REASONED; traefik-docs): Inspect ACME startup errors; failed issuance can leave a self-signed TRAEFIK DEFAULT CERT. +- canary-route (REASONED; docker, traefik-docs): On a disposable deployment, unlabelled traefik/whoami has one HTTP port; use its generated Host rule and plain HTTP even on :443 because the generated router requests no TLS. +- verify-canary (REASONED; docker, curl): Calibrate exposedByDefault true with 200 and the canary Hostname body; false should give 404. Missing discovery/provider also gives 404, unreachable backend 502/504. +- verify-app-control (REASONED; docker, traefik-docs, curl): In the same run, the app must return 401 or known authenticated 200 while the canary gives 404; app 401 proves routing, not proxy-auth attachment. Keep TLS verification. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "aac661357effc89cf9b208294d0f255b4fb31c03428609d9756c4fa69347809c", + "components": { + "traefik-v3": { + "name": "Traefik v3 documentation", + "basis": "v3.5", + "sources": { + "sd6ad276040b7": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/entrypoints/", + "s418a6b1ad09b": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/tls/certificate-resolvers/acme/", + "s25d849b2d428": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/tls/tls-options/", + "s3acb3f5b1778": "https://doc.traefik.io/traefik/v3.5/reference/install-configuration/providers/others/file/", + "s5d7a6940f1dd": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/middlewares/basicauth/", + "s3cf7d994a56a": "https://doc.traefik.io/traefik/v3.5/reference/routing-configuration/http/middlewares/forwardauth/" + } + }, + "traefik-source": { + "name": "Traefik ACME storage source", + "basis": "v3.5.0", + "sources": { + "s0e08b0bdcb35": "https://github.com/traefik/traefik/blob/v3.5.0/pkg/provider/acme/local_store.go#L125" + } + }, + "traefik-basicauth-source": { + "name": "Traefik BasicAuth source", + "basis": "v3.5.0", + "sources": { + "s5f9a13d720c8": "https://github.com/traefik/traefik/blob/v3.5.0/pkg/middlewares/auth/basic_auth.go#L67-L99" + } + }, + "traefik-docs": { + "name": "Traefik documentation (rolling)", + "basis": "unknown", + "sources": { + "scbc0f2135c78": "https://doc.traefik.io/traefik/" + } + }, + "docker": { + "name": "Traefik Docker provider", + "basis": "unknown", + "sources": { + "se23fe4fe79fa": "https://doc.traefik.io/traefik/reference/install-configuration/providers/docker/" + } + }, + "swarm": { + "name": "Traefik Swarm provider", + "basis": "unknown", + "sources": { + "s256d46f3f558": "https://doc.traefik.io/traefik/reference/install-configuration/providers/swarm/" + } + }, + "middleware": { + "name": "Traefik middleware documentation", + "basis": "unknown", + "sources": { + "sd987195d64dd": "https://doc.traefik.io/traefik/middlewares/http/buffering/", + "sd20dcba7ebca": "https://doc.traefik.io/traefik/middlewares/http/inflightreq/", + "scf1b1e35ef5d": "https://doc.traefik.io/traefik/middlewares/http/ratelimit/" + } + }, + "httpd-source": { + "name": "Apache httpd htpasswd source", + "basis": "2.4.69", + "sources": { + "sf711558bdf3c": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L45", + "s4aa0f7f341fb": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L292-L295", + "s4482c55d79a2": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L382", + "s309ecdd34379": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L128" + } + }, + "cloudflare-source": { + "name": "Cloudflare Access MFA documentation", + "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81" + } + }, + "curl": { + "name": "curl write-out minimum", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + }, + "owasp-source": { + "name": "OWASP Password Storage Cheat Sheet", + "basis": "72865b1e3b5754246a101778dc00b37f9c233373", + "sources": { + "s4531e65b6502": "https://github.com/OWASP/CheatSheetSeries/blob/72865b1e3b5754246a101778dc00b37f9c233373/cheatsheets/Password_Storage_Cheat_Sheet.md#L13" + } + } + }, + "claims": { + "entrypoints": {"text": "For Traefik v2/v3, web :80 redirects to HTTPS websecure :443 and an ACME resolver obtains/renews certificates.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:sd6ad276040b7", "traefik-v3:s418a6b1ad09b"], "status": "REASONED"}, + "acme-storage": {"text": "Persist acme.json across restarts with mode 600 to retain certificates and avoid repeated issuance/rate limits.", "components": ["traefik-docs", "traefik-v3", "traefik-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s418a6b1ad09b", "traefik-source:s0e08b0bdcb35"], "status": "REASONED"}, + "acme-challenges": {"text": "TLS-ALPN requires inbound 443; HTTP-01 uses 80, while DNS supports wildcards without inbound ports.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s418a6b1ad09b"], "status": "REASONED"}, + "docker-default": {"text": "Docker discovery defaults exposedByDefault to true; set false and opt each intended container in with traefik.enable=true.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, + "docker-scope": {"text": "Discovery is daemon-wide, not Compose-project scoped; eligible unlabelled containers get routers unless excluded. Missing usable ports skip service creation.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, + "docker-socket": {"text": "The Docker socket gives daemon control despite :ro; use a restricted socket proxy to limit API access.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, + "tls-floor": {"text": "Dynamic default TLS options set minVersion VersionTLS12.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s25d849b2d428"], "status": "REASONED"}, + "route": {"text": "Docker labels select the app host, websecure, letsencrypt resolver and backend port 3000.", "components": ["traefik-docs", "docker"], "sources": ["traefik-docs:scbc0f2135c78", "docker:se23fe4fe79fa"], "status": "REASONED"}, + "backend-isolation": {"text": "Publish only Traefik 80/443, not the app's 3000, to prevent middleware/TLS bypass.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED"}, + "basic": {"text": "Attach bcrypt basicAuth to the router; Apache httpd 2.4.69 htpasswd -nB defaults to bcrypt cost 5, while -C 12 selects cost 12; OWASP at 72865b1e recommends 10 or more for legacy bcrypt.", "components": ["traefik-docs", "traefik-v3", "httpd-source", "owasp-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s5d7a6940f1dd", "httpd-source:sf711558bdf3c", "httpd-source:s4aa0f7f341fb", "httpd-source:s4482c55d79a2", "httpd-source:s309ecdd34379", "owasp-source:s4531e65b6502"], "status": "REASONED"}, + "compose-hash": {"text": "Double each dollar sign in Compose hash labels; file-provider hashes need no such escaping.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s5d7a6940f1dd"], "status": "REASONED"}, + "file-provider": {"text": "Load dynamic files with providers.file.directory/filename and reference their middleware from Docker as app-auth@file.", "components": ["traefik-docs", "traefik-v3"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s3acb3f5b1778"], "status": "REASONED"}, + "mfa": {"text": "Basic is single-factor; use forwardAuth with Authelia/oauth2-proxy or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization).", "components": ["traefik-docs", "traefik-v3", "cloudflare-source"], "sources": ["traefik-docs:scbc0f2135c78", "traefik-v3:s3cf7d994a56a", "cloudflare-source:sdcac732541da"], "status": "REASONED"}, + "body-limit": {"text": "Attach buffering.maxRequestBodyBytes=10485760 alongside existing auth/routing labels; it bounds accepted body size.", "components": ["middleware"], "sources": ["middleware:sd987195d64dd"], "status": "REASONED"}, + "buffer-storage": {"text": "memRequestBodyBytes defaults to 1048576 and controls the memory-to-disk threshold independently of maximum accepted body size.", "components": ["middleware"], "sources": ["middleware:sd987195d64dd"], "status": "REASONED"}, + "rate-limit": {"text": "average=10 and burst=20 configure a rate cap, per source by default.", "components": ["middleware"], "sources": ["middleware:scf1b1e35ef5d"], "status": "REASONED"}, + "inflight": {"text": "inflightreq.amount=10 bounds concurrent requests; sourceCriterion defaults to request host rather than client, so configure a per-client cap explicitly.", "components": ["middleware"], "sources": ["middleware:sd20dcba7ebca"], "status": "REASONED"}, + "middleware-order": {"text": "Rate/inflight precede auth to count rejected logins: Traefik v3.5.0 BasicAuth returns on failure without calling the next handler; buffering follows admission controls to avoid buffering rejected uploads.", "components": ["traefik-docs", "middleware", "traefik-basicauth-source"], "sources": ["traefik-docs:scbc0f2135c78", "middleware:sd987195d64dd", "middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca", "traefik-basicauth-source:s5f9a13d720c8"], "status": "REASONED"}, + "dashboard": {"text": "Keep api.insecure and unprotected api@internal routers off public entry points or protect the dashboard with auth.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED"}, + "swarm": {"text": "Standalone Docker guidance does not cover Swarm; v3 uses providers.swarm and its separate exposedByDefault also defaults true.", "components": ["swarm"], "sources": ["swarm:s256d46f3f558"], "status": "REASONED"}, + "verify-redirect": {"text": "HTTP should redirect to HTTPS.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "Unauthenticated HTTPS should return 401 once auth is attached.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, + "verify-size": {"text": "Authenticated 1M must reach the app and 11M return 413; 401/000 invalidate the control. Remove app-body in isolation to rule out backend refusal.", "components": ["middleware", "traefik-docs"], "sources": ["middleware:sd987195d64dd", "traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, + "observed-429": {"text": "The guide records only a 429 response; no limiter attribution or exposed/fixed demonstration is established.", "components": ["middleware"], "sources": ["middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca"], "status": "DEMONSTRATED", "evidence": "A 429 appeared. That is all this shows."}, + "verify-rate": {"text": "Concurrent unauthenticated probes can reach pre-auth limiters; 429 alone cannot distinguish rate, inflight or upstream refusal. Compare each limiter alone against both disabled.", "components": ["middleware", "traefik-basicauth-source"], "sources": ["middleware:scf1b1e35ef5d", "middleware:sd20dcba7ebca", "traefik-basicauth-source:s5f9a13d720c8"], "status": "REASONED", "verify": [1]}, + "verify-isolation": {"text": "Compose ps covers one project; ss misses NAT-only publication without a userland proxy. Neither finds unintended routes through Traefik.", "components": ["docker"], "sources": ["docker:se23fe4fe79fa"], "status": "REASONED", "verify": [1]}, + "verify-acme": {"text": "Inspect ACME startup errors; failed issuance can leave a self-signed TRAEFIK DEFAULT CERT.", "components": ["traefik-docs"], "sources": ["traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [1]}, + "canary-route": {"text": "On a disposable deployment, unlabelled traefik/whoami has one HTTP port; use its generated Host rule and plain HTTP even on :443 because the generated router requests no TLS.", "components": ["docker", "traefik-docs"], "sources": ["docker:se23fe4fe79fa", "traefik-docs:scbc0f2135c78"], "status": "REASONED", "verify": [2]}, + "verify-canary": {"text": "Calibrate exposedByDefault true with 200 and the canary Hostname body; false should give 404. Missing discovery/provider also gives 404, unreachable backend 502/504.", "components": ["docker", "curl"], "sources": ["docker:se23fe4fe79fa", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]}, + "verify-app-control": {"text": "In the same run, the app must return 401 or known authenticated 200 while the canary gives 404; app 401 proves routing, not proxy-auth attachment. Keep TLS verification.", "components": ["docker", "traefik-docs", "curl"], "sources": ["docker:se23fe4fe79fa", "traefik-docs:scbc0f2135c78", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [3]} + } +} +```` + diff --git a/workflow-orchestrators.md b/workflow-orchestrators.md index 7d1af98fe..6d4c2be64 100644 --- a/workflow-orchestrators.md +++ b/workflow-orchestrators.md @@ -1,451 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-05", - "documentation_checked": "2026-10", - "body_sha256": "fa18bb5bca5189881826e754d7d45b7a5179143b73c35dd0f654f46b3e7945b1", - "components": { - "prefect": { - "name": "Prefect Basic Auth minimum", - "basis": "3.1.8", - "sources": { - "s12416d1a0d69": "https://github.com/PrefectHQ/prefect/releases/tag/3.1.8" - } - }, - "prefect-source": { - "name": "Prefect server source", - "basis": "9e560c9b6df4e19a5109a66e66d461f9facb538d", - "sources": { - "sa4574c7ed80f": "https://github.com/PrefectHQ/prefect/blob/9e560c9b6df4e19a5109a66e66d461f9facb538d/src/prefect/server/api/server.py" - } - }, - "dagster": { - "name": "Dagster OSS", - "basis": "1.13.24", - "sources": { - "sab1b42d92f27": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L41-L42", - "s6bc57f86dfe1": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L81-L96", - "s2455602a504b": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L306-L311", - "s3c7d8f17f245": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster/dagster/_cli/dev.py#L251-L252", - "s469c458c4a71": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L339-L351", - "s695dfd19dcca": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/templates/helpers/_deployment-webserver.tpl#L86-L90", - "sf6f027e96a0d": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/templates/helpers/_helpers.tpl#L55", - "seda3a1b24425": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/values.yaml#L54-L58" - } - }, - "airflow": { - "name": "Apache Airflow", - "basis": "3.3.2", - "sources": { - "s1d0b86fe4334": "https://airflow.apache.org/docs/apache-airflow/3.3.2/security/security_model.html", - "sad02a49561a0": "https://airflow.apache.org/docs/apache-airflow/3.3.2/core-concepts/auth-manager/simple/index.html", - "s3be3e4616150": "https://airflow.apache.org/docs/apache-airflow/3.3.2/core-concepts/auth-manager/index.html", - "s78c661193a1a": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#simple-auth-manager-all-admins", - "s963b49f58a13": "https://airflow.apache.org/docs/apache-airflow/3.3.2/security/api.html", - "s705f1eb5de00": "https://raw.githubusercontent.com/apache/airflow/3.3.2/airflow-core/docs/howto/docker-compose/docker-compose.yaml", - "sdfcfd543928c": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#secret-key", - "s4ed64009ed8f": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#expose-config" - } - }, - "airflow-two": { - "name": "Apache Airflow historical configuration", - "basis": "2.11.0", - "sources": { - "sb97f3044e215": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#auth-backends", - "sabb71378ad74": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#secret-key", - "s6813801b7880": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#expose-config" - } - }, - "airflow-old": { - "name": "Apache Airflow historical default", - "basis": "2.2.5", - "sources": { - "s4e72b1f51038": "https://airflow.apache.org/docs/apache-airflow/2.2.5/configurations-ref.html#auth-backend" - } - }, - "flower": { - "name": "Flower", - "basis": "v2.2.0", - "sources": { - "s25760fae7152": "https://github.com/mher/flower/blob/v2.2.0/flower/options.py#L7-L15", - "s1f72a8415337": "https://github.com/mher/flower/blob/v2.2.0/flower/options.py#L56-L57", - "s4dd518442b7f": "https://github.com/mher/flower/blob/v2.2.0/flower/command.py#L39-L91", - "s4e49ebc71dc1": "https://github.com/mher/flower/blob/v2.2.0/flower/app.py#L71-L76", - "s0fe6c77e9c9d": "https://github.com/mher/flower/blob/v2.2.0/flower/app.py#L85-L89" - } - }, - "tornado": { - "name": "Tornado", - "basis": "v6.5.10", - "sources": { - "sc503b4425831": "https://github.com/tornadoweb/tornado/blob/v6.5.10/tornado/netutil.py#L72-L73", - "s737c51cccd00": "https://github.com/tornadoweb/tornado/blob/v6.5.10/tornado/netutil.py#L215-L228" - } - }, - "linux": { - "name": "Linux permissions and tools", - "basis": "unknown", - "sources": { - "s8ffd93b0efa4": "https://man7.org/linux/man-pages/man7/unix.7.html", - "sfb53d2819cd0": "https://man7.org/linux/man-pages/man7/path_resolution.7.html", - "s31f41c54750c": "https://man7.org/linux/man-pages/man5/acl.5.html", - "s3cf3d5be0f64": "https://man7.org/linux/man-pages/man7/user_namespaces.7.html", - "sa5ccbfea6e16": "https://www.kernel.org/doc/html/latest/filesystems/idmappings.html", - "s08902025e5f9": "https://man7.org/linux/man-pages/man1/getfacl.1.html", - "sa0d3793fabb7": "https://man7.org/linux/man-pages/man1/stat.1.html" - } - }, - "systemd": { - "name": "systemd", - "basis": "v257", - "sources": { - "s48ece26c16d3": "https://github.com/systemd/systemd/blob/v257/man/systemd.exec.xml#L1630-L1640" - } - }, - "fhs": { - "name": "Filesystem Hierarchy Standard", - "basis": "3.0", - "sources": { - "seafd8219f367": "https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch03s15.html" - } - }, - "docker": { - "name": "Moby Docker", - "basis": "docker-v29.8.1", - "sources": { - "s1f268e247a1f": "https://github.com/moby/moby/blob/docker-v29.8.1/daemon/pkg/oci/caps/defaults.go#L4-L7" - } - }, - "argo-three": { - "name": "Argo Workflows", - "basis": "v3.7.18", - "sources": { - "s262098553d2f": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/cmd/argo/commands/server.go#L191-L229", - "sbddb9f07a301": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/apiserver/argoserver.go#L263-L282", - "s2ca4f14c3164": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/gatekeeper.go#L166-L218", - "sfbb904619bfa": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/overlays/argo-server-deployment.yaml#L1-L15", - "s0ac72aa1a86c": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterole.yaml#L1-L65", - "s1689f25010be": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/cluster-workflow-template-rbac.yaml#L1-L58", - "sf0c3a52d3302": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/Makefile#L493-L515", - "sfdcdf614ee37": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/base/argo-server/argo-server-deployment.yaml#L14-L34", - "sc7ca3a37b16c": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/overlays/argo-server-deployment.yaml#L1-L4", - "s01f102470d97": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/base/argo-server/argo-server-service.yaml#L1-L11", - "s49cdaf0691c0": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/mode.go#L33-L44", - "sdb4a0c18b7c6": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/cmd/argo/commands/server.go#L113-L137", - "s36759aec55fa": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/apiserver/argoserver.go#L430-L449", - "s862033e52c9b": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-minimal.yaml#L3613-L3664", - "s73e9beeef775": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-mysql.yaml#L3678-L3729", - "s476ff15db454": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-postgres.yaml#L3677-L3728", - "sa4b2fa875915": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterolebinding.yaml#L1-L11", - "s48db5ba07d23": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/kustomization.yaml#L4-L5", - "se3ffdf2973a6": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/argo-server-rbac/argo-server-role.yaml#L1-L65", - "sb901e7f87b54": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/argo-server-rbac/argo-server-rolebinding.yaml#L1-L11", - "s49a8855b6da2": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/docs/argo-server-auth-mode.md#L3-L9", - "s64e74b0dbe95": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/gatekeeper.go#L106-L121" - } - }, - "argo-four": { - "name": "Argo Workflows", - "basis": "v4.1.4", - "sources": { - "s774215022573": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/cmd/argo/commands/server.go#L195-L239", - "s0f074a9ac21e": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/apiserver/argoserver.go#L299-L320", - "sb284f93cde9c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/gatekeeper.go#L189-L244", - "s0871a1dd9b13": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/docs/argo-server-auth-mode.md#L3-L9", - "sa7b9db8384be": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start/base/overlays/argo-server-deployment.yaml#L1-L15", - "s79648b46d3f5": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-telemetry.yaml#L192919-L192970", - "sd2706d04971d": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterole.yaml#L1-L67", - "sd920da4ad4b7": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/docs/quick-start.md#L3-L34", - "s144b824252c1": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/Makefile#L608-L634", - "s51645bfbc47c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/base/argo-server/argo-server-deployment.yaml#L14-L34", - "s5188f6f2efbb": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/overlays/argo-server-deployment.yaml#L1-L4", - "s639e9fe0024e": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/base/argo-server/argo-server-service.yaml#L1-L11", - "s4613c15c72af": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/mode.go#L33-L44", - "s0b16f1ac50df": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/cmd/argo/commands/server.go#L119-L143", - "sa120f74372eb": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/apiserver/argoserver.go#L476-L494", - "s6c08a5f34f2c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-minimal.yaml#L192772-L192823", - "sa0bf6187d1db": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-mysql.yaml#L192838-L192889", - "s31f4a149576b": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-postgres.yaml#L192837-L192888", - "s3be9b03e29be": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterolebinding.yaml#L1-L11", - "sc263bd937cf0": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start/base/kustomization.yaml#L4-L5", - "s1b134f96428f": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/argo-server-rbac/argo-server-role.yaml#L1-L67", - "s2418ee1aa49d": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/argo-server-rbac/argo-server-rolebinding.yaml#L1-L11", - "se758d83aaf1f": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/gatekeeper.go#L133-L148" - } - }, - "argo-docs": { - "name": "Argo Workflows documentation", - "basis": "release-3.7", - "sources": { - "s6c8e78288632": "https://argo-workflows.readthedocs.io/en/release-3.7/security/", - "s9898d4653a47": "https://argo-workflows.readthedocs.io/en/release-3.7/argo-server-sso/", - "s96145eacf345": "https://argo-workflows.readthedocs.io/en/release-3.7/rest-examples/" - } - }, - "iproute2": { - "name": "iproute2 ss manual", - "basis": "v6.15.0", - "sources": { - "s6cc7f285a0c7": "https://github.com/iproute2/iproute2/blob/v6.15.0/man/man8/ss.8#L358-L359", - "s9fa9a260f1de": "https://github.com/iproute2/iproute2/blob/v6.15.0/man/man8/ss.8#L7-L12" - } - }, - "network-namespaces": { - "name": "Linux network namespaces manual", - "basis": "man-pages-5.13", - "sources": { - "s427c923e346f": "https://github.com/mkerrisk/man-pages/blob/man-pages-5.13/man7/network_namespaces.7#L30-L40" - } - }, - "prefect-rolling": { - "name": "Prefect documentation (rolling)", - "basis": "unknown", - "sources": { - "sbe64d51110a2": "https://docs.prefect.io/v3/how-to-guides/self-hosted/server-cli", - "s0ef73a227667": "https://docs.prefect.io/v3/advanced/security-settings" - } - }, - "dagster-rolling": { - "name": "Dagster documentation (rolling)", - "basis": "unknown", - "sources": { - "s4c04d5da5257": "https://docs.dagster.io/guides/operate/webserver" - } - }, - "airflow-rolling": { - "name": "Apache Airflow documentation (rolling)", - "basis": "unknown", - "sources": { - "s2d1891641c91": "https://airflow.apache.org/docs/apache-airflow/stable/security/", - "s39bfd91c507b": "https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html" - } - }, - "temporal-rolling": { - "name": "Temporal documentation (rolling)", - "basis": "unknown", - "sources": { - "s12f2c64ad396": "https://docs.temporal.io/cli/command-reference/server", - "s06d4f0dbd0c8": "https://docs.temporal.io/self-hosted-guide/security", - "sa9d5614385ca": "https://docs.temporal.io/references/web-ui-configuration" - } - }, - "flower-rolling": { - "name": "Flower documentation (rolling)", - "basis": "unknown", - "sources": { - "sb1ba69f79c9b": "https://flower.readthedocs.io/en/latest/config.html" - } - }, - "prefect-blocks": { - "name": "Prefect Blocks source", - "basis": "3.1.8", - "sources": { - "s56081101d16a": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/server/api/block_documents.py#L47-L70", - "s1eae2c5f9f5e": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/server/api/block_documents.py#L101-L119", - "s46e69a2090c4": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/blocks/system.py#L154-L159" - } - }, - "fab-source": { - "name": "Apache Airflow FAB provider source", - "basis": "providers-fab/3.9.0", - "sources": { - "s3c94f0f7176a": "https://github.com/apache/airflow/blob/providers-fab/3.9.0/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py#L327-L381", - "s5552a28ab9d8": "https://github.com/apache/airflow/blob/providers-fab/3.9.0/providers/fab/pyproject.toml#L75-L81" - } - }, - "airflow-crypto": { - "name": "Apache Airflow crypto source", - "basis": "3.3.2", - "sources": { - "sdeaf2f54c054": "https://github.com/apache/airflow/blob/3.3.2/airflow-core/src/airflow/models/crypto.py#L47-L68", - "s70ea7631e820": "https://github.com/apache/airflow/blob/3.3.2/airflow-core/src/airflow/models/crypto.py#L154-L173" - } - }, - "temporal-codec": { - "name": "Temporal Go SDK codec source", - "basis": "v1.28.0", - "sources": { - "s24435f531da0": "https://github.com/temporalio/sdk-go/blob/v1.28.0/converter/codec.go#L284-L350" - } - }, - "fab-rolling": { - "name": "Apache Airflow FAB provider documentation (rolling)", - "basis": "unknown", - "sources": { - "s989f09d05161": "https://airflow.apache.org/docs/apache-airflow-providers-fab/stable/auth-manager/api-authentication.html" - } - } - }, - "claims": { - "fronting": {"text": "Keep UI/API origins private behind HTTPS and authentication; use IdP MFA for browsers and separate machine authentication, and constrain worker egress.", "components": ["airflow", "argo-docs", "temporal-rolling"], "sources": ["airflow:s1d0b86fe4334", "temporal-rolling:s06d4f0dbd0c8", "argo-docs:s6c8e78288632"], "status": "REASONED"}, - "prefect-auth": {"text": "Self-hosted Prefect defaults unauthenticated; Basic Auth from 3.1.8 uses matching server/client auth strings and prompts the UI.", "components": ["prefect", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, - "prefect-key": {"text": "Cloud-only PREFECT_API_KEY takes precedence over PREFECT_API_AUTH_STRING and produces 401 against a self-hosted server.", "components": ["prefect", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, - "prefect-secrets": {"text": "Protect the auth string and credential-bearing Blocks; Prefect 3.1.8 block-document read routes accept include_secrets, and Secret.get() returns the underlying value. Restrict API and worker access; UI masking does not enforce authorization.", "components": ["prefect-blocks", "prefect-rolling"], "sources": ["prefect-blocks:s56081101d16a", "prefect-blocks:s1eae2c5f9f5e", "prefect-blocks:s46e69a2090c4", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, - "prefect-port": {"text": "The self-hosted Prefect server uses port 4200.", "components": ["prefect-rolling"], "sources": ["prefect-rolling:sbe64d51110a2"], "status": "REASONED"}, - "dagster-bind": {"text": "Direct dagster-webserver/dev defaults to 127.0.0.1:3000, with flag/environment overrides and free-port fallback when the port is unset or 0; explicitly bind the private origin.", "components": ["dagster"], "sources": ["dagster:sab1b42d92f27", "dagster:s6bc57f86dfe1", "dagster:s2455602a504b", "dagster:s3c7d8f17f245", "dagster:s469c458c4a71"], "status": "REASONED"}, - "dagster-helm": {"text": "The chart passes -h 0.0.0.0 on the Service port, default 80, with ClusterIP Service by default.", "components": ["dagster"], "sources": ["dagster:s695dfd19dcca", "dagster:sf6f027e96a0d", "dagster:seda3a1b24425"], "status": "REASONED"}, - "dagster-auth": {"text": "OSS webserver has no built-in login or access control; protect all routes with an authenticating proxy.", "components": ["dagster-rolling"], "sources": ["dagster-rolling:s4c04d5da5257"], "status": "REASONED"}, - "airflow-boundary": {"text": "Airflow assumes authenticated known users and is not designed for untrusted public exposure; deployment managers must keep it private.", "components": ["airflow"], "sources": ["airflow:s1d0b86fe4334"], "status": "REASONED"}, - "airflow-two-api": {"text": "Airflow 2.11.0 uses [api] auth_backends with session default; before 2.3 the name was auth_backend and 2.2.5 defaulted to deny_all.", "components": ["airflow-two", "airflow-old"], "sources": ["airflow-two:sb97f3044e215", "airflow-old:s4e72b1f51038"], "status": "REASONED"}, - "airflow-simple": {"text": "Airflow 3 defaults to development-only Simple Auth Manager, with configured users/roles and generated passwords printed to logs unless supplied.", "components": ["airflow"], "sources": ["airflow:sad02a49561a0", "airflow:s3be3e4616150"], "status": "REASONED"}, - "airflow-fab": {"text": "For production install FAB and select FabAuthManager through [core] auth_manager; verify the effective manager and use an identity backend with MFA.", "components": ["airflow", "fab-rolling"], "sources": ["airflow:s3be3e4616150", "fab-rolling:s989f09d05161"], "status": "REASONED"}, - "airflow-api": {"text": "Airflow 3 public API uses JWT independently of [fab] auth_backends, which selects FAB API backends rather than the auth manager.", "components": ["airflow", "fab-rolling"], "sources": ["airflow:s3be3e4616150", "fab-rolling:s989f09d05161", "airflow:s963b49f58a13"], "status": "REASONED"}, - "airflow-all-admins": {"text": "Keep simple_auth_manager_all_admins unset or False; enabling it disables login and grants every visitor admin.", "components": ["airflow"], "sources": ["airflow:sad02a49561a0", "airflow:s78c661193a1a"], "status": "REASONED"}, - "fab-public-role": {"text": "Leave [fab] auth_role_public and legacy AUTH_ROLE_PUBLIC unset; a configured public role can make FAB treat an anonymous visitor as logged in and supply that role's permissions.", "components": ["fab-source"], "sources": ["fab-source:s3c94f0f7176a", "fab-source:s5552a28ab9d8"], "status": "REASONED"}, - "airflow-compose-account": {"text": "Development Compose selects FAB but defaults to airflow/airflow; set credentials before account creation and update/delete an existing account separately.", "components": ["airflow-rolling", "fab-rolling", "airflow"], "sources": ["airflow-rolling:s39bfd91c507b", "fab-rolling:s989f09d05161", "airflow:s705f1eb5de00"], "status": "REASONED"}, - "airflow-jwt": {"text": "Replace Compose fallback AIRFLOW__API_AUTH__JWT_SECRET=airflow_jwt_secret and share the strong value with all signers/validators; the public fallback permits token forgery.", "components": ["airflow-rolling", "airflow"], "sources": ["airflow-rolling:s39bfd91c507b", "airflow:s705f1eb5de00"], "status": "REASONED"}, - "airflow-secret-key": {"text": "Provision the independent secret_key too: [webserver] on 2.11.0 and [api] on 3.3.2.", "components": ["airflow-two", "airflow-rolling", "airflow"], "sources": ["airflow-rolling:s2d1891641c91", "airflow-two:sabb71378ad74", "airflow:sdfcfd543928c"], "status": "REASONED"}, - "airflow-config": {"text": "Keep configuration exposure off with WEBSERVER__EXPOSE_CONFIG on 2.11.0 or API__EXPOSE_CONFIG on 3.3.2.", "components": ["airflow-two", "airflow-rolling", "airflow"], "sources": ["airflow-rolling:s2d1891641c91", "airflow-two:s6813801b7880", "airflow:s4ed64009ed8f"], "status": "REASONED"}, - "airflow-fernet": {"text": "Protect Connections, Variables, database and Fernet keys; an empty [core] FERNET_KEY selects a no-encryption implementation for new values, while configured comma-separated keys use MultiFernet. Preserve keys needed to decrypt existing values; authorized workloads still use secrets.", "components": ["airflow-crypto", "airflow-rolling"], "sources": ["airflow-crypto:sdeaf2f54c054", "airflow-crypto:s70ea7631e820", "airflow-rolling:s2d1891641c91"], "status": "REASONED"}, - "airflow-port": {"text": "Compose publishes 8080 on all interfaces; use 127.0.0.1:8080:8080 behind the fronting layer.", "components": ["airflow-rolling"], "sources": ["airflow-rolling:s39bfd91c507b"], "status": "REASONED"}, - "temporal-authorizer": {"text": "Empty authorizer selects noopAuthorizer allowing every API request, including administration; configure default authorization with trusted JWT keys and audience.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, - "temporal-claims": {"text": "Empty claimMapper selects a no-op granting system-admin claims; configure both Authorizer and ClaimMapper, with frontend TLS.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, - "temporal-health": {"text": "The built-in default authorizer permits health checks without claims; health is not an authorization discriminator.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, - "temporal-ui": {"text": "UI OIDC auth.enabled is a sibling of providers; TEMPORAL_AUTH_ENABLED plus provider settings gates the UI only, with MFA at the IdP.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:sa9d5614385ca"], "status": "REASONED"}, - "temporal-tls": {"text": "Internode and frontend mTLS are separate from UI login and API authorization.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, - "temporal-payloads": {"text": "Keep plaintext credentials out of persisted inputs/results/history; the Temporal Go SDK v1.28.0 codec HTTP handler accepts encode/decode POSTs without an authentication check, so authenticate and restrict a deployed Codec Server separately.", "components": ["temporal-rolling", "temporal-codec"], "sources": ["temporal-rolling:s06d4f0dbd0c8", "temporal-codec:s24435f531da0"], "status": "REASONED"}, - "temporal-ports": {"text": "Frontend gRPC uses 7233 and the Web UI 8233 in the cited CLI server reference.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s12f2c64ad396"], "status": "REASONED"}, - "flower-bind": {"text": "Flower defaults to wildcard address and 5555 unless Unix socket, FLOWER_ variables or implicit working-directory flowerconfig.py override; bind loopback explicitly.", "components": ["flower", "tornado"], "sources": ["flower:s25760fae7152", "flower:s1f72a8415337", "flower:s4dd518442b7f", "flower:s4e49ebc71dc1", "tornado:sc503b4425831"], "status": "REASONED"}, - "flower-api": {"text": "Authentication defaults off; unauthenticated HTTP API is disabled unless FLOWER_UNAUTHENTICATED_API=true, which must not be mistaken for dashboard protection.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, - "flower-basic": {"text": "Basic auth uses a comma-separated credential list; supply it through FLOWER_BASIC_AUTH or protected config rather than argv and retain private TLS fronting.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, - "flower-oauth": {"text": "OAuth needs handler, client key/secret, redirect and allowed-email regex; protect the secret outside argv and prefer an MFA-enforcing provider.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, - "flower-socket": {"text": "Flower passes fixed mode 0777; Tornado removes/recreates filesystem sockets on startup, so chmod does not persist. Abstract sockets have no filesystem permission boundary.", "components": ["flower", "tornado", "linux"], "sources": ["flower:s0fe6c77e9c9d", "tornado:s737c51cccd00", "linux:s8ffd93b0efa4"], "status": "REASONED"}, - "flower-directory": {"text": "Restrict socket directory search with Flower ownership, proxy-only group, mode 0750 and no outsider ACL; directory access is essential because socket mode permits writes.", "components": ["linux"], "sources": ["linux:sfb53d2819cd0", "linux:s8ffd93b0efa4", "linux:s31f41c54750c"], "status": "REASONED"}, - "flower-capabilities": {"text": "Applicable DAC capabilities bypass directory search checks; Docker defaults include CAP_DAC_OVERRIDE, while user namespaces and idmapped mounts change identity interpretation.", "components": ["linux", "docker"], "sources": ["linux:s3cf3d5be0f64", "linux:sa5ccbfea6e16", "docker:s1f268e247a1f"], "status": "REASONED"}, - "flower-runtime-dir": {"text": "Reapply directory owner/group/mode on creation: /run is cleared at boot and RuntimeDirectory defaults 0755 unless RuntimeDirectoryMode overrides it.", "components": ["systemd", "fhs"], "sources": ["systemd:s48ece26c16d3", "fhs:seafd8219f367"], "status": "REASONED"}, - "flower-numeric-ids": {"text": "A proxy sharing the socket directory must hold the required numeric group after namespace/mount mappings; names alone do not establish access.", "components": ["linux"], "sources": ["linux:s3cf3d5be0f64", "linux:sa5ccbfea6e16"], "status": "REASONED"}, - "argo-bind": {"text": "Argo Server binds wildcard :2746 with host-dependent IPv4/IPv6 support and no bind-address flag.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s262098553d2f", "argo-four:s774215022573", "argo-three:sbddb9f07a301", "argo-four:s0f074a9ac21e"], "status": "REASONED"}, - "argo-defaults": {"text": "Both tags default secure=true, auth-mode=client and hsts=true; flags and ARGO_ environment equivalents can override.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s262098553d2f", "argo-four:s774215022573"], "status": "REASONED"}, - "argo-client": {"text": "Client mode uses caller Kubernetes credentials/RBAC; v4.1.4 validates via SelfSubjectReview and requires Kubernetes 1.28+.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c", "argo-four:s0871a1dd9b13"], "status": "REASONED"}, - "argo-quickstart": {"text": "Both tags' minimal/mysql/postgres quick-starts enable server plus client mode while retaining TLS; v4.1.4 telemetry does too. Anonymous callers inherit server permissions.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sfbb904619bfa", "argo-four:sa7b9db8384be", "argo-four:s79648b46d3f5", "argo-three:s862033e52c9b", "argo-three:s73e9beeef775", "argo-three:s476ff15db454", "argo-three:s262098553d2f", "argo-three:s49cdaf0691c0", "argo-three:s2ca4f14c3164", "argo-four:s6c08a5f34f2c", "argo-four:sa0bf6187d1db", "argo-four:s31f4a149576b", "argo-four:s774215022573", "argo-four:s4613c15c72af", "argo-four:sb284f93cde9c"], "status": "REASONED"}, - "argo-quickstart-rbac": {"text": "Quick-start binds cluster-wide workflow/template and Secret get/create permissions, plus cluster-workflow-template rights; it is demo-only.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s0ac72aa1a86c", "argo-four:sd2706d04971d", "argo-three:s1689f25010be", "argo-four:sd920da4ad4b7", "argo-three:sa4b2fa875915", "argo-three:s48db5ba07d23", "argo-four:s3be9b03e29be", "argo-four:sc263bd937cf0"], "status": "REASONED"}, - "argo-installs": {"text": "Traced install inputs keep client auth/TLS with cluster-wide server RBAC; namespace-install adds --namespaced and namespace RBAC. Generated install YAML was absent.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sf0c3a52d3302", "argo-four:s144b824252c1", "argo-three:sfdcdf614ee37", "argo-four:s51645bfbc47c", "argo-three:sc7ca3a37b16c", "argo-four:s5188f6f2efbb", "argo-three:s262098553d2f", "argo-three:s0ac72aa1a86c", "argo-three:sa4b2fa875915", "argo-three:se3ffdf2973a6", "argo-three:sb901e7f87b54", "argo-four:s774215022573", "argo-four:sd2706d04971d", "argo-four:s3be9b03e29be", "argo-four:s1b134f96428f", "argo-four:s2418ee1aa49d"], "status": "REASONED"}, - "argo-service": {"text": "Base Service omits type, defaulting to ClusterIP, and maps 2746 to 2746; this does not authenticate reachable callers.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s01f102470d97", "argo-four:s639e9fe0024e"], "status": "REASONED"}, - "argo-fallback": {"text": "Remove server auth mode: adding client/SSO does not prevent anonymous fallback to server credentials; local mode uses the server kubeconfig.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s49cdaf0691c0", "argo-four:s4613c15c72af", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c", "argo-four:s0871a1dd9b13", "argo-three:s49a8855b6da2"], "status": "REASONED"}, - "argo-sso": {"text": "Enable sso.rbac.enabled and map groups to narrow ServiceAccounts through rbac-rule/precedence; without SSO RBAC, authenticated users share server permissions.", "components": ["argo-docs", "argo-three", "argo-four"], "sources": ["argo-docs:s9898d4653a47", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c"], "status": "REASONED"}, - "argo-secrets": {"text": "Store SSO OAuth credentials in referenced Kubernetes Secrets and supply API tokens through protected input, not argv.", "components": ["argo-docs"], "sources": ["argo-docs:s9898d4653a47", "argo-docs:s96145eacf345"], "status": "REASONED"}, - "argo-tls": {"text": "Keep TLS enabled with a trusted certificate Secret; no name generates a self-signed certificate, failed named-Secret loading fails startup, secure=false is plaintext and defeats HSTS.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sdb4a0c18b7c6", "argo-four:s0b16f1ac50df", "argo-three:s36759aec55fa", "argo-four:sa120f74372eb"], "status": "REASONED"}, - "argo-metrics": {"text": "/metrics shares the server listener and auth gate unless ARGO_SERVER_METRICS_AUTH=false; server mode also admits anonymous metrics.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s36759aec55fa", "argo-four:sa120f74372eb", "argo-three:s49cdaf0691c0", "argo-three:s2ca4f14c3164", "argo-four:s4613c15c72af", "argo-four:sb284f93cde9c", "argo-three:s64e74b0dbe95", "argo-four:se758d83aaf1f"], "status": "REASONED"}, - "argo-network": {"text": "Keep 2746 private behind authenticated HTTPS; restrict direct Pod/Service access and scope server, SSO and execution-account permissions. Workflow submission permits arbitrary containers unless constrained.", "components": ["argo-docs"], "sources": ["argo-docs:s6c8e78288632"], "status": "REASONED"}, - "verify-inventory": {"text": "ss inventories only the current namespace, not publication, routing or authentication; inspect publications and external reachability separately.", "components": ["flower", "argo-three", "iproute2", "network-namespaces", "prefect-rolling"], "sources": ["prefect-rolling:sbe64d51110a2", "flower:s25760fae7152", "argo-three:sbddb9f07a301", "iproute2:s6cc7f285a0c7", "network-namespaces:s427c923e346f", "iproute2:s9fa9a260f1de"], "status": "REASONED", "verify": [1]}, - "verify-prefect": {"text": "Anonymous POST /api/flows/filter returning a JSON list is exposed; fixed is 401 with an authorized list on the same origin. Health/ready GET exemptions are version-dependent.", "components": ["prefect", "prefect-source", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-source:sa4574c7ed80f", "prefect-rolling:s0ef73a227667"], "status": "REASONED", "verify": [1]}, - "verify-dagster": {"text": "A RepositoryConnection from /graphql is a read, including empty nodes; GraphQL/transport errors are inconclusive. Test proxy authentication and origin isolation separately.", "components": ["dagster-rolling"], "sources": ["dagster-rolling:s4c04d5da5257"], "status": "REASONED", "verify": [1]}, - "verify-airflow": {"text": "FAB 3.9.0 returning 201 with access_token for airflow/airflow is exposed; pair a valid account and anonymous/authenticated API reads at proxy and origin. Airflow 2 uses its own API/auth.", "components": ["airflow-two", "airflow-rolling", "fab-rolling"], "sources": ["fab-rolling:s989f09d05161", "airflow-rolling:s39bfd91c507b", "airflow-two:sb97f3044e215"], "status": "REASONED", "verify": [1]}, - "verify-temporal": {"text": "Credential-free workflow listing on 7233 is exposed; require rejection and a matched authorized call, not health, TLS errors or missing namespaces as proof of auth.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8", "temporal-rolling:s12f2c64ad396"], "status": "REASONED", "verify": [1]}, - "verify-flower": {"text": "Anonymous UI/API should yield Basic 401 or OAuth login redirect, with a valid-session control; API-disabled is not proof the dashboard is protected.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED", "verify": [1]}, - "verify-socket": {"text": "Directory/socket permissions, outsider denial, reboot persistence and container numeric IDs remain reasoned; no socket or Flower process ran.", "components": ["flower", "linux"], "sources": ["flower:s0fe6c77e9c9d", "linux:s31f41c54750c", "linux:s08902025e5f9", "linux:sa0d3793fabb7"], "status": "REASONED", "verify": [1]}, - "flower-stat-run": {"text": "Tmpfs directory-only stat distinguished 0755 and 0750; the 0750 directory initially had only base ACL entries.", "components": ["linux"], "sources": ["linux:sa0d3793fabb7", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "`stat -c '%a %U:%G %u:%g %n'` printed `755` for a directory created with mode `0755` and `750` for one created with `0750`; `getfacl -p` on the `0750` directory listed only `user::rwx`, `group::r-x` and `other::---`."}, - "flower-acl-run": {"text": "A named nobody search ACL remained invisible in stat mode 750; ls and getfacl exposed the entry.", "components": ["linux"], "sources": ["linux:s31f41c54750c", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "After `setfacl -m u:nobody:--x` on the `0750` directory, `stat` still printed `750`. Only the trailing `+` in `ls -ld` and getfacl's `user:nobody:--x` line showed the named entry."}, - "flower-mask-run": {"text": "Changing the ACL mask hid named search permission; chmod 0750 restored its reported effectiveness. Access as nobody was not tested.", "components": ["linux"], "sources": ["linux:s31f41c54750c", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "After `setfacl -m m::r--`, getfacl printed `user:nobody:--x` with `#effective:---`. While masked, `stat` printed `740`."}, - "verify-argo": {"text": "Anonymous workflow JSON is exposed when server RBAC permits; missing tokens should yield 401 without server mode. A 403 alone is inconclusive; pair authorized 200 and inspect auth modes.", "components": ["argo-three", "argo-four", "argo-docs"], "sources": ["argo-three:s49cdaf0691c0", "argo-four:s4613c15c72af", "argo-docs:s96145eacf345", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c"], "status": "REASONED", "verify": [1]}, - "verify-origin": {"text": "Any HTTP response from the origin at an untrusted vantage proves publication; use the same responding allowed origin as control and treat connection errors/timeouts as inconclusive.", "components": ["airflow", "argo-docs"], "sources": ["airflow:s1d0b86fe4334", "argo-docs:s6c8e78288632"], "status": "REASONED", "verify": [1]} - } -} ---- -# Workflow and agent orchestrators: Prefect, Dagster, Airflow, Temporal, Flower, Argo - - -**Version basis** - -AI assistants must compare these versions with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. - -Metadata reviewed 2026-10-05; documentation checked 2026-10 (exact day unknown). DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. - -Components (recorded basis): - -- prefect: Prefect Basic Auth minimum (3.1.8) -- prefect-source: Prefect server source (9e560c9b6df4e19a5109a66e66d461f9facb538d) -- dagster: Dagster OSS (1.13.24) -- airflow: Apache Airflow (3.3.2) -- airflow-two: Apache Airflow historical configuration (2.11.0) -- airflow-old: Apache Airflow historical default (2.2.5) -- flower: Flower (v2.2.0) -- tornado: Tornado (v6.5.10) -- linux: Linux permissions and tools (unknown) -- systemd: systemd (v257) -- fhs: Filesystem Hierarchy Standard (3.0) -- docker: Moby Docker (docker-v29.8.1) -- argo-three: Argo Workflows (v3.7.18) -- argo-four: Argo Workflows (v4.1.4) -- argo-docs: Argo Workflows documentation (release-3.7) -- iproute2: iproute2 ss manual (v6.15.0) -- network-namespaces: Linux network namespaces manual (man-pages-5.13) -- prefect-rolling: Prefect documentation (rolling) (unknown) -- dagster-rolling: Dagster documentation (rolling) (unknown) -- airflow-rolling: Apache Airflow documentation (rolling) (unknown) -- temporal-rolling: Temporal documentation (rolling) (unknown) -- flower-rolling: Flower documentation (rolling) (unknown) -- prefect-blocks: Prefect Blocks source (3.1.8) -- fab-source: Apache Airflow FAB provider source (providers-fab/3.9.0) -- airflow-crypto: Apache Airflow crypto source (3.3.2) -- temporal-codec: Temporal Go SDK codec source (v1.28.0) -- fab-rolling: Apache Airflow FAB provider documentation (rolling) (unknown) +# Workflow and agent orchestrators: Prefect, Dagster, Airflow, Temporal, Flower, Argo -Claims (full source mapping in the front matter): +Written against Prefect Basic Auth minimum 3.1.8, Prefect Blocks source 3.1.8, Dagster OSS 1.13.24, Apache Airflow 3.3.2, Apache Airflow historical configuration 2.11.0, Apache Airflow historical default 2.2.5, and Apache Airflow FAB provider source providers-fab/3.9.0, plus 11 more recorded bases; 1 further source is pinned to a commit and 7 carry no recorded version. Metadata reviewed 2026-10-05; documentation checked October 2026. The guide records 62 claims: 3 demonstrated by a check that ran and 59 reasoned from cited sources. -- fronting (REASONED; airflow, argo-docs, temporal-rolling): Keep UI/API origins private behind HTTPS and authentication; use IdP MFA for browsers and separate machine authentication, and constrain worker egress. -- prefect-auth (REASONED; prefect, prefect-rolling): Self-hosted Prefect defaults unauthenticated; Basic Auth from 3.1.8 uses matching server/client auth strings and prompts the UI. -- prefect-key (REASONED; prefect, prefect-rolling): Cloud-only PREFECT_API_KEY takes precedence over PREFECT_API_AUTH_STRING and produces 401 against a self-hosted server. -- prefect-secrets (REASONED; prefect-blocks, prefect-rolling): Protect the auth string and credential-bearing Blocks; Prefect 3.1.8 block-document read routes accept include_secrets, and Secret.get() returns the underlying value. Restrict API and worker access; UI masking does not enforce authorization. -- prefect-port (REASONED; prefect-rolling): The self-hosted Prefect server uses port 4200. -- dagster-bind (REASONED; dagster): Direct dagster-webserver/dev defaults to 127.0.0.1:3000, with flag/environment overrides and free-port fallback when the port is unset or 0; explicitly bind the private origin. -- dagster-helm (REASONED; dagster): The chart passes -h 0.0.0.0 on the Service port, default 80, with ClusterIP Service by default. -- dagster-auth (REASONED; dagster-rolling): OSS webserver has no built-in login or access control; protect all routes with an authenticating proxy. -- airflow-boundary (REASONED; airflow): Airflow assumes authenticated known users and is not designed for untrusted public exposure; deployment managers must keep it private. -- airflow-two-api (REASONED; airflow-two, airflow-old): Airflow 2.11.0 uses [api] auth_backends with session default; before 2.3 the name was auth_backend and 2.2.5 defaulted to deny_all. -- airflow-simple (REASONED; airflow): Airflow 3 defaults to development-only Simple Auth Manager, with configured users/roles and generated passwords printed to logs unless supplied. -- airflow-fab (REASONED; airflow, fab-rolling): For production install FAB and select FabAuthManager through [core] auth_manager; verify the effective manager and use an identity backend with MFA. -- airflow-api (REASONED; airflow, fab-rolling): Airflow 3 public API uses JWT independently of [fab] auth_backends, which selects FAB API backends rather than the auth manager. -- airflow-all-admins (REASONED; airflow): Keep simple_auth_manager_all_admins unset or False; enabling it disables login and grants every visitor admin. -- fab-public-role (REASONED; fab-source): Leave [fab] auth_role_public and legacy AUTH_ROLE_PUBLIC unset; a configured public role can make FAB treat an anonymous visitor as logged in and supply that role's permissions. -- airflow-compose-account (REASONED; airflow-rolling, fab-rolling, airflow): Development Compose selects FAB but defaults to airflow/airflow; set credentials before account creation and update/delete an existing account separately. -- airflow-jwt (REASONED; airflow-rolling, airflow): Replace Compose fallback AIRFLOW__API_AUTH__JWT_SECRET=airflow_jwt_secret and share the strong value with all signers/validators; the public fallback permits token forgery. -- airflow-secret-key (REASONED; airflow-two, airflow-rolling, airflow): Provision the independent secret_key too: [webserver] on 2.11.0 and [api] on 3.3.2. -- airflow-config (REASONED; airflow-two, airflow-rolling, airflow): Keep configuration exposure off with WEBSERVER__EXPOSE_CONFIG on 2.11.0 or API__EXPOSE_CONFIG on 3.3.2. -- airflow-fernet (REASONED; airflow-crypto, airflow-rolling): Protect Connections, Variables, database and Fernet keys; an empty [core] FERNET_KEY selects a no-encryption implementation for new values, while configured comma-separated keys use MultiFernet. Preserve keys needed to decrypt existing values; authorized workloads still use secrets. -- airflow-port (REASONED; airflow-rolling): Compose publishes 8080 on all interfaces; use 127.0.0.1:8080:8080 behind the fronting layer. -- temporal-authorizer (REASONED; temporal-rolling): Empty authorizer selects noopAuthorizer allowing every API request, including administration; configure default authorization with trusted JWT keys and audience. -- temporal-claims (REASONED; temporal-rolling): Empty claimMapper selects a no-op granting system-admin claims; configure both Authorizer and ClaimMapper, with frontend TLS. -- temporal-health (REASONED; temporal-rolling): The built-in default authorizer permits health checks without claims; health is not an authorization discriminator. -- temporal-ui (REASONED; temporal-rolling): UI OIDC auth.enabled is a sibling of providers; TEMPORAL_AUTH_ENABLED plus provider settings gates the UI only, with MFA at the IdP. -- temporal-tls (REASONED; temporal-rolling): Internode and frontend mTLS are separate from UI login and API authorization. -- temporal-payloads (REASONED; temporal-rolling, temporal-codec): Keep plaintext credentials out of persisted inputs/results/history; the Temporal Go SDK v1.28.0 codec HTTP handler accepts encode/decode POSTs without an authentication check, so authenticate and restrict a deployed Codec Server separately. -- temporal-ports (REASONED; temporal-rolling): Frontend gRPC uses 7233 and the Web UI 8233 in the cited CLI server reference. -- flower-bind (REASONED; flower, tornado): Flower defaults to wildcard address and 5555 unless Unix socket, FLOWER_ variables or implicit working-directory flowerconfig.py override; bind loopback explicitly. -- flower-api (REASONED; flower-rolling): Authentication defaults off; unauthenticated HTTP API is disabled unless FLOWER_UNAUTHENTICATED_API=true, which must not be mistaken for dashboard protection. -- flower-basic (REASONED; flower-rolling): Basic auth uses a comma-separated credential list; supply it through FLOWER_BASIC_AUTH or protected config rather than argv and retain private TLS fronting. -- flower-oauth (REASONED; flower-rolling): OAuth needs handler, client key/secret, redirect and allowed-email regex; protect the secret outside argv and prefer an MFA-enforcing provider. -- flower-socket (REASONED; flower, tornado, linux): Flower passes fixed mode 0777; Tornado removes/recreates filesystem sockets on startup, so chmod does not persist. Abstract sockets have no filesystem permission boundary. -- flower-directory (REASONED; linux): Restrict socket directory search with Flower ownership, proxy-only group, mode 0750 and no outsider ACL; directory access is essential because socket mode permits writes. -- flower-capabilities (REASONED; linux, docker): Applicable DAC capabilities bypass directory search checks; Docker defaults include CAP_DAC_OVERRIDE, while user namespaces and idmapped mounts change identity interpretation. -- flower-runtime-dir (REASONED; systemd, fhs): Reapply directory owner/group/mode on creation: /run is cleared at boot and RuntimeDirectory defaults 0755 unless RuntimeDirectoryMode overrides it. -- flower-numeric-ids (REASONED; linux): A proxy sharing the socket directory must hold the required numeric group after namespace/mount mappings; names alone do not establish access. -- argo-bind (REASONED; argo-three, argo-four): Argo Server binds wildcard :2746 with host-dependent IPv4/IPv6 support and no bind-address flag. -- argo-defaults (REASONED; argo-three, argo-four): Both tags default secure=true, auth-mode=client and hsts=true; flags and ARGO_ environment equivalents can override. -- argo-client (REASONED; argo-three, argo-four): Client mode uses caller Kubernetes credentials/RBAC; v4.1.4 validates via SelfSubjectReview and requires Kubernetes 1.28+. -- argo-quickstart (REASONED; argo-three, argo-four): Both tags' minimal/mysql/postgres quick-starts enable server plus client mode while retaining TLS; v4.1.4 telemetry does too. Anonymous callers inherit server permissions. -- argo-quickstart-rbac (REASONED; argo-three, argo-four): Quick-start binds cluster-wide workflow/template and Secret get/create permissions, plus cluster-workflow-template rights; it is demo-only. -- argo-installs (REASONED; argo-three, argo-four): Traced install inputs keep client auth/TLS with cluster-wide server RBAC; namespace-install adds --namespaced and namespace RBAC. Generated install YAML was absent. -- argo-service (REASONED; argo-three, argo-four): Base Service omits type, defaulting to ClusterIP, and maps 2746 to 2746; this does not authenticate reachable callers. -- argo-fallback (REASONED; argo-three, argo-four): Remove server auth mode: adding client/SSO does not prevent anonymous fallback to server credentials; local mode uses the server kubeconfig. -- argo-sso (REASONED; argo-docs, argo-three, argo-four): Enable sso.rbac.enabled and map groups to narrow ServiceAccounts through rbac-rule/precedence; without SSO RBAC, authenticated users share server permissions. -- argo-secrets (REASONED; argo-docs): Store SSO OAuth credentials in referenced Kubernetes Secrets and supply API tokens through protected input, not argv. -- argo-tls (REASONED; argo-three, argo-four): Keep TLS enabled with a trusted certificate Secret; no name generates a self-signed certificate, failed named-Secret loading fails startup, secure=false is plaintext and defeats HSTS. -- argo-metrics (REASONED; argo-three, argo-four): /metrics shares the server listener and auth gate unless ARGO_SERVER_METRICS_AUTH=false; server mode also admits anonymous metrics. -- argo-network (REASONED; argo-docs): Keep 2746 private behind authenticated HTTPS; restrict direct Pod/Service access and scope server, SSO and execution-account permissions. Workflow submission permits arbitrary containers unless constrained. -- verify-inventory (REASONED; flower, argo-three, iproute2, network-namespaces, prefect-rolling): ss inventories only the current namespace, not publication, routing or authentication; inspect publications and external reachability separately. -- verify-prefect (REASONED; prefect, prefect-source, prefect-rolling): Anonymous POST /api/flows/filter returning a JSON list is exposed; fixed is 401 with an authorized list on the same origin. Health/ready GET exemptions are version-dependent. -- verify-dagster (REASONED; dagster-rolling): A RepositoryConnection from /graphql is a read, including empty nodes; GraphQL/transport errors are inconclusive. Test proxy authentication and origin isolation separately. -- verify-airflow (REASONED; airflow-two, airflow-rolling, fab-rolling): FAB 3.9.0 returning 201 with access_token for airflow/airflow is exposed; pair a valid account and anonymous/authenticated API reads at proxy and origin. Airflow 2 uses its own API/auth. -- verify-temporal (REASONED; temporal-rolling): Credential-free workflow listing on 7233 is exposed; require rejection and a matched authorized call, not health, TLS errors or missing namespaces as proof of auth. -- verify-flower (REASONED; flower-rolling): Anonymous UI/API should yield Basic 401 or OAuth login redirect, with a valid-session control; API-disabled is not proof the dashboard is protected. -- verify-socket (REASONED; flower, linux): Directory/socket permissions, outsider denial, reboot persistence and container numeric IDs remain reasoned; no socket or Flower process ran. -- flower-stat-run (DEMONSTRATED; linux): Tmpfs directory-only stat distinguished 0755 and 0750; the 0750 directory initially had only base ACL entries. -- flower-acl-run (DEMONSTRATED; linux): A named nobody search ACL remained invisible in stat mode 750; ls and getfacl exposed the entry. -- flower-mask-run (DEMONSTRATED; linux): Changing the ACL mask hid named search permission; chmod 0750 restored its reported effectiveness. Access as nobody was not tested. -- verify-argo (REASONED; argo-three, argo-four, argo-docs): Anonymous workflow JSON is exposed when server RBAC permits; missing tokens should yield 401 without server mode. A 403 alone is inconclusive; pair authorized 200 and inspect auth modes. -- verify-origin (REASONED; airflow, argo-docs): Any HTTP response from the origin at an untrusted vantage proves publication; use the same responding allowed origin as control and treat connection errors/timeouts as inconclusive. - +A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under [requests/](requests/README.md), or reach the maintainer as [CONTRIBUTING.md](CONTRIBUTING.md) describes. The machine-readable version basis and claim list are in [the section for AI assistants](#about-the-ai-assistant-section-below) at the end of this guide. These webservers and UIs schedule and trigger arbitrary code execution across your infrastructure, and most ship with no authentication at all. Keep every one of them off the public internet and add auth before @@ -919,3 +476,472 @@ These defaults are checked against Prefect 3.1.8+ for Basic Auth, Dagster 1.13.x - Apache Airflow development Compose FAB selection, administrator credentials and JWT fallback secret (pinned tag 3.3.2): https://raw.githubusercontent.com/apache/airflow/3.3.2/airflow-core/docs/howto/docker-compose/docker-compose.yaml - Apache Airflow webserver secret_key and EXPOSE_CONFIG settings (2.11.0 documentation): https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#secret-key, https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#expose-config - Apache Airflow API secret_key and EXPOSE_CONFIG settings (3.3.2 documentation): https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#secret-key, https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#expose-config + + + +## About the AI assistant section below + +The rest of this file is generated by the repository's tooling and written for AI assistants rather than for people. It records the versions this guide was written against, the guide's claims with a status for each (demonstrated by a check that ran, or reasoned from the cited sources), and the mapping from each claim to its sources. You are welcome to read it, but it is dense and can be hard to take in. + +If an AI assistant is helping you secure a deployment, give it the prompt below: + +```text +Read the secureconfig guide at +https://raw.githubusercontent.com/jposluns/secureconfig/main/workflow-orchestrators.md +in full, including the section for AI assistants at the end. Then, for my +deployment: state which of its recommendations apply and the exact changes +to make; compare the versions the guide was written against with the +releases I am running and with the vendor's current releases, and flag any +guidance a newer release may have changed; and run or adapt the guide's +Verify steps where you can. List separately every item you could not +verify instead of reporting it as passed. +``` + +This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard. + +**Version basis** + +AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist. + +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. + +Components (recorded basis): + +- prefect: Prefect Basic Auth minimum (3.1.8) +- prefect-blocks: Prefect Blocks source (3.1.8) +- dagster: Dagster OSS (1.13.24) +- airflow: Apache Airflow (3.3.2) +- airflow-two: Apache Airflow historical configuration (2.11.0) +- airflow-old: Apache Airflow historical default (2.2.5) +- fab-source: Apache Airflow FAB provider source (providers-fab/3.9.0) +- airflow-crypto: Apache Airflow crypto source (3.3.2) +- temporal-codec: Temporal Go SDK codec source (v1.28.0) +- flower: Flower (v2.2.0) +- argo-three: Argo Workflows (v3.7.18) +- argo-four: Argo Workflows (v4.1.4) +- argo-docs: Argo Workflows documentation (release-3.7) +- prefect-source: Prefect server source (9e560c9b6df4e19a5109a66e66d461f9facb538d) +- prefect-rolling: Prefect documentation (rolling) (unknown) +- dagster-rolling: Dagster documentation (rolling) (unknown) +- airflow-rolling: Apache Airflow documentation (rolling) (unknown) +- fab-rolling: Apache Airflow FAB provider documentation (rolling) (unknown) +- temporal-rolling: Temporal documentation (rolling) (unknown) +- flower-rolling: Flower documentation (rolling) (unknown) +- tornado: Tornado (v6.5.10) +- docker: Moby Docker (docker-v29.8.1) +- linux: Linux permissions and tools (unknown) +- systemd: systemd (v257) +- fhs: Filesystem Hierarchy Standard (3.0) +- iproute2: iproute2 ss manual (v6.15.0) +- network-namespaces: Linux network namespaces manual (man-pages-5.13) + +Claims (full source mapping in the JSON block below): + +- fronting (REASONED; airflow, argo-docs, temporal-rolling): Keep UI/API origins private behind HTTPS and authentication; use IdP MFA for browsers and separate machine authentication, and constrain worker egress. +- prefect-auth (REASONED; prefect, prefect-rolling): Self-hosted Prefect defaults unauthenticated; Basic Auth from 3.1.8 uses matching server/client auth strings and prompts the UI. +- prefect-key (REASONED; prefect, prefect-rolling): Cloud-only PREFECT_API_KEY takes precedence over PREFECT_API_AUTH_STRING and produces 401 against a self-hosted server. +- prefect-secrets (REASONED; prefect-blocks, prefect-rolling): Protect the auth string and credential-bearing Blocks; Prefect 3.1.8 block-document read routes accept include_secrets, and Secret.get() returns the underlying value. Restrict API and worker access; UI masking does not enforce authorization. +- prefect-port (REASONED; prefect-rolling): The self-hosted Prefect server uses port 4200. +- dagster-bind (REASONED; dagster): Direct dagster-webserver/dev defaults to 127.0.0.1:3000, with flag/environment overrides and free-port fallback when the port is unset or 0; explicitly bind the private origin. +- dagster-helm (REASONED; dagster): The chart passes -h 0.0.0.0 on the Service port, default 80, with ClusterIP Service by default. +- dagster-auth (REASONED; dagster-rolling): OSS webserver has no built-in login or access control; protect all routes with an authenticating proxy. +- airflow-boundary (REASONED; airflow): Airflow assumes authenticated known users and is not designed for untrusted public exposure; deployment managers must keep it private. +- airflow-two-api (REASONED; airflow-two, airflow-old): Airflow 2.11.0 uses [api] auth_backends with session default; before 2.3 the name was auth_backend and 2.2.5 defaulted to deny_all. +- airflow-simple (REASONED; airflow): Airflow 3 defaults to development-only Simple Auth Manager, with configured users/roles and generated passwords printed to logs unless supplied. +- airflow-fab (REASONED; airflow, fab-rolling): For production install FAB and select FabAuthManager through [core] auth_manager; verify the effective manager and use an identity backend with MFA. +- airflow-api (REASONED; airflow, fab-rolling): Airflow 3 public API uses JWT independently of [fab] auth_backends, which selects FAB API backends rather than the auth manager. +- airflow-all-admins (REASONED; airflow): Keep simple_auth_manager_all_admins unset or False; enabling it disables login and grants every visitor admin. +- fab-public-role (REASONED; fab-source): Leave [fab] auth_role_public and legacy AUTH_ROLE_PUBLIC unset; a configured public role can make FAB treat an anonymous visitor as logged in and supply that role's permissions. +- airflow-compose-account (REASONED; airflow-rolling, fab-rolling, airflow): Development Compose selects FAB but defaults to airflow/airflow; set credentials before account creation and update/delete an existing account separately. +- airflow-jwt (REASONED; airflow-rolling, airflow): Replace Compose fallback AIRFLOW__API_AUTH__JWT_SECRET=airflow_jwt_secret and share the strong value with all signers/validators; the public fallback permits token forgery. +- airflow-secret-key (REASONED; airflow-two, airflow-rolling, airflow): Provision the independent secret_key too: [webserver] on 2.11.0 and [api] on 3.3.2. +- airflow-config (REASONED; airflow-two, airflow-rolling, airflow): Keep configuration exposure off with WEBSERVER__EXPOSE_CONFIG on 2.11.0 or API__EXPOSE_CONFIG on 3.3.2. +- airflow-fernet (REASONED; airflow-crypto, airflow-rolling): Protect Connections, Variables, database and Fernet keys; an empty [core] FERNET_KEY selects a no-encryption implementation for new values, while configured comma-separated keys use MultiFernet. Preserve keys needed to decrypt existing values; authorized workloads still use secrets. +- airflow-port (REASONED; airflow-rolling): Compose publishes 8080 on all interfaces; use 127.0.0.1:8080:8080 behind the fronting layer. +- temporal-authorizer (REASONED; temporal-rolling): Empty authorizer selects noopAuthorizer allowing every API request, including administration; configure default authorization with trusted JWT keys and audience. +- temporal-claims (REASONED; temporal-rolling): Empty claimMapper selects a no-op granting system-admin claims; configure both Authorizer and ClaimMapper, with frontend TLS. +- temporal-health (REASONED; temporal-rolling): The built-in default authorizer permits health checks without claims; health is not an authorization discriminator. +- temporal-ui (REASONED; temporal-rolling): UI OIDC auth.enabled is a sibling of providers; TEMPORAL_AUTH_ENABLED plus provider settings gates the UI only, with MFA at the IdP. +- temporal-tls (REASONED; temporal-rolling): Internode and frontend mTLS are separate from UI login and API authorization. +- temporal-payloads (REASONED; temporal-rolling, temporal-codec): Keep plaintext credentials out of persisted inputs/results/history; the Temporal Go SDK v1.28.0 codec HTTP handler accepts encode/decode POSTs without an authentication check, so authenticate and restrict a deployed Codec Server separately. +- temporal-ports (REASONED; temporal-rolling): Frontend gRPC uses 7233 and the Web UI 8233 in the cited CLI server reference. +- flower-bind (REASONED; flower, tornado): Flower defaults to wildcard address and 5555 unless Unix socket, FLOWER_ variables or implicit working-directory flowerconfig.py override; bind loopback explicitly. +- flower-api (REASONED; flower-rolling): Authentication defaults off; unauthenticated HTTP API is disabled unless FLOWER_UNAUTHENTICATED_API=true, which must not be mistaken for dashboard protection. +- flower-basic (REASONED; flower-rolling): Basic auth uses a comma-separated credential list; supply it through FLOWER_BASIC_AUTH or protected config rather than argv and retain private TLS fronting. +- flower-oauth (REASONED; flower-rolling): OAuth needs handler, client key/secret, redirect and allowed-email regex; protect the secret outside argv and prefer an MFA-enforcing provider. +- flower-socket (REASONED; flower, tornado, linux): Flower passes fixed mode 0777; Tornado removes/recreates filesystem sockets on startup, so chmod does not persist. Abstract sockets have no filesystem permission boundary. +- flower-directory (REASONED; linux): Restrict socket directory search with Flower ownership, proxy-only group, mode 0750 and no outsider ACL; directory access is essential because socket mode permits writes. +- flower-capabilities (REASONED; linux, docker): Applicable DAC capabilities bypass directory search checks; Docker defaults include CAP_DAC_OVERRIDE, while user namespaces and idmapped mounts change identity interpretation. +- flower-runtime-dir (REASONED; systemd, fhs): Reapply directory owner/group/mode on creation: /run is cleared at boot and RuntimeDirectory defaults 0755 unless RuntimeDirectoryMode overrides it. +- flower-numeric-ids (REASONED; linux): A proxy sharing the socket directory must hold the required numeric group after namespace/mount mappings; names alone do not establish access. +- argo-bind (REASONED; argo-three, argo-four): Argo Server binds wildcard :2746 with host-dependent IPv4/IPv6 support and no bind-address flag. +- argo-defaults (REASONED; argo-three, argo-four): Both tags default secure=true, auth-mode=client and hsts=true; flags and ARGO_ environment equivalents can override. +- argo-client (REASONED; argo-three, argo-four): Client mode uses caller Kubernetes credentials/RBAC; v4.1.4 validates via SelfSubjectReview and requires Kubernetes 1.28+. +- argo-quickstart (REASONED; argo-three, argo-four): Both tags' minimal/mysql/postgres quick-starts enable server plus client mode while retaining TLS; v4.1.4 telemetry does too. Anonymous callers inherit server permissions. +- argo-quickstart-rbac (REASONED; argo-three, argo-four): Quick-start binds cluster-wide workflow/template and Secret get/create permissions, plus cluster-workflow-template rights; it is demo-only. +- argo-installs (REASONED; argo-three, argo-four): Traced install inputs keep client auth/TLS with cluster-wide server RBAC; namespace-install adds --namespaced and namespace RBAC. Generated install YAML was absent. +- argo-service (REASONED; argo-three, argo-four): Base Service omits type, defaulting to ClusterIP, and maps 2746 to 2746; this does not authenticate reachable callers. +- argo-fallback (REASONED; argo-three, argo-four): Remove server auth mode: adding client/SSO does not prevent anonymous fallback to server credentials; local mode uses the server kubeconfig. +- argo-sso (REASONED; argo-docs, argo-three, argo-four): Enable sso.rbac.enabled and map groups to narrow ServiceAccounts through rbac-rule/precedence; without SSO RBAC, authenticated users share server permissions. +- argo-secrets (REASONED; argo-docs): Store SSO OAuth credentials in referenced Kubernetes Secrets and supply API tokens through protected input, not argv. +- argo-tls (REASONED; argo-three, argo-four): Keep TLS enabled with a trusted certificate Secret; no name generates a self-signed certificate, failed named-Secret loading fails startup, secure=false is plaintext and defeats HSTS. +- argo-metrics (REASONED; argo-three, argo-four): /metrics shares the server listener and auth gate unless ARGO_SERVER_METRICS_AUTH=false; server mode also admits anonymous metrics. +- argo-network (REASONED; argo-docs): Keep 2746 private behind authenticated HTTPS; restrict direct Pod/Service access and scope server, SSO and execution-account permissions. Workflow submission permits arbitrary containers unless constrained. +- verify-inventory (REASONED; flower, argo-three, iproute2, network-namespaces, prefect-rolling): ss inventories only the current namespace, not publication, routing or authentication; inspect publications and external reachability separately. +- verify-prefect (REASONED; prefect, prefect-source, prefect-rolling): Anonymous POST /api/flows/filter returning a JSON list is exposed; fixed is 401 with an authorized list on the same origin. Health/ready GET exemptions are version-dependent. +- verify-dagster (REASONED; dagster-rolling): A RepositoryConnection from /graphql is a read, including empty nodes; GraphQL/transport errors are inconclusive. Test proxy authentication and origin isolation separately. +- verify-airflow (REASONED; airflow-two, airflow-rolling, fab-rolling): FAB 3.9.0 returning 201 with access_token for airflow/airflow is exposed; pair a valid account and anonymous/authenticated API reads at proxy and origin. Airflow 2 uses its own API/auth. +- verify-temporal (REASONED; temporal-rolling): Credential-free workflow listing on 7233 is exposed; require rejection and a matched authorized call, not health, TLS errors or missing namespaces as proof of auth. +- verify-flower (REASONED; flower-rolling): Anonymous UI/API should yield Basic 401 or OAuth login redirect, with a valid-session control; API-disabled is not proof the dashboard is protected. +- verify-socket (REASONED; flower, linux): Directory/socket permissions, outsider denial, reboot persistence and container numeric IDs remain reasoned; no socket or Flower process ran. +- flower-stat-run (DEMONSTRATED; linux): Tmpfs directory-only stat distinguished 0755 and 0750; the 0750 directory initially had only base ACL entries. +- flower-acl-run (DEMONSTRATED; linux): A named nobody search ACL remained invisible in stat mode 750; ls and getfacl exposed the entry. +- flower-mask-run (DEMONSTRATED; linux): Changing the ACL mask hid named search permission; chmod 0750 restored its reported effectiveness. Access as nobody was not tested. +- verify-argo (REASONED; argo-three, argo-four, argo-docs): Anonymous workflow JSON is exposed when server RBAC permits; missing tokens should yield 401 without server mode. A 403 alone is inconclusive; pair authorized 200 and inspect auth modes. +- verify-origin (REASONED; airflow, argo-docs): Any HTTP response from the origin at an untrusted vantage proves publication; use the same responding allowed origin as control and treat connection errors/timeouts as inconclusive. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-10", + "body_sha256": "fa18bb5bca5189881826e754d7d45b7a5179143b73c35dd0f654f46b3e7945b1", + "components": { + "prefect": { + "name": "Prefect Basic Auth minimum", + "basis": "3.1.8", + "sources": { + "s12416d1a0d69": "https://github.com/PrefectHQ/prefect/releases/tag/3.1.8" + } + }, + "prefect-blocks": { + "name": "Prefect Blocks source", + "basis": "3.1.8", + "sources": { + "s56081101d16a": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/server/api/block_documents.py#L47-L70", + "s1eae2c5f9f5e": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/server/api/block_documents.py#L101-L119", + "s46e69a2090c4": "https://github.com/PrefectHQ/prefect/blob/3.1.8/src/prefect/blocks/system.py#L154-L159" + } + }, + "dagster": { + "name": "Dagster OSS", + "basis": "1.13.24", + "sources": { + "sab1b42d92f27": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L41-L42", + "s6bc57f86dfe1": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L81-L96", + "s2455602a504b": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L306-L311", + "s3c7d8f17f245": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster/dagster/_cli/dev.py#L251-L252", + "s469c458c4a71": "https://github.com/dagster-io/dagster/blob/1.13.24/python_modules/dagster-webserver/dagster_webserver/cli.py#L339-L351", + "s695dfd19dcca": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/templates/helpers/_deployment-webserver.tpl#L86-L90", + "sf6f027e96a0d": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/templates/helpers/_helpers.tpl#L55", + "seda3a1b24425": "https://github.com/dagster-io/dagster/blob/1.13.24/helm/dagster/values.yaml#L54-L58" + } + }, + "airflow": { + "name": "Apache Airflow", + "basis": "3.3.2", + "sources": { + "s1d0b86fe4334": "https://airflow.apache.org/docs/apache-airflow/3.3.2/security/security_model.html", + "sad02a49561a0": "https://airflow.apache.org/docs/apache-airflow/3.3.2/core-concepts/auth-manager/simple/index.html", + "s3be3e4616150": "https://airflow.apache.org/docs/apache-airflow/3.3.2/core-concepts/auth-manager/index.html", + "s78c661193a1a": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#simple-auth-manager-all-admins", + "s963b49f58a13": "https://airflow.apache.org/docs/apache-airflow/3.3.2/security/api.html", + "s705f1eb5de00": "https://raw.githubusercontent.com/apache/airflow/3.3.2/airflow-core/docs/howto/docker-compose/docker-compose.yaml", + "sdfcfd543928c": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#secret-key", + "s4ed64009ed8f": "https://airflow.apache.org/docs/apache-airflow/3.3.2/configurations-ref.html#expose-config" + } + }, + "airflow-two": { + "name": "Apache Airflow historical configuration", + "basis": "2.11.0", + "sources": { + "sb97f3044e215": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#auth-backends", + "sabb71378ad74": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#secret-key", + "s6813801b7880": "https://airflow.apache.org/docs/apache-airflow/2.11.0/configurations-ref.html#expose-config" + } + }, + "airflow-old": { + "name": "Apache Airflow historical default", + "basis": "2.2.5", + "sources": { + "s4e72b1f51038": "https://airflow.apache.org/docs/apache-airflow/2.2.5/configurations-ref.html#auth-backend" + } + }, + "fab-source": { + "name": "Apache Airflow FAB provider source", + "basis": "providers-fab/3.9.0", + "sources": { + "s3c94f0f7176a": "https://github.com/apache/airflow/blob/providers-fab/3.9.0/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py#L327-L381", + "s5552a28ab9d8": "https://github.com/apache/airflow/blob/providers-fab/3.9.0/providers/fab/pyproject.toml#L75-L81" + } + }, + "airflow-crypto": { + "name": "Apache Airflow crypto source", + "basis": "3.3.2", + "sources": { + "sdeaf2f54c054": "https://github.com/apache/airflow/blob/3.3.2/airflow-core/src/airflow/models/crypto.py#L47-L68", + "s70ea7631e820": "https://github.com/apache/airflow/blob/3.3.2/airflow-core/src/airflow/models/crypto.py#L154-L173" + } + }, + "temporal-codec": { + "name": "Temporal Go SDK codec source", + "basis": "v1.28.0", + "sources": { + "s24435f531da0": "https://github.com/temporalio/sdk-go/blob/v1.28.0/converter/codec.go#L284-L350" + } + }, + "flower": { + "name": "Flower", + "basis": "v2.2.0", + "sources": { + "s25760fae7152": "https://github.com/mher/flower/blob/v2.2.0/flower/options.py#L7-L15", + "s1f72a8415337": "https://github.com/mher/flower/blob/v2.2.0/flower/options.py#L56-L57", + "s4dd518442b7f": "https://github.com/mher/flower/blob/v2.2.0/flower/command.py#L39-L91", + "s4e49ebc71dc1": "https://github.com/mher/flower/blob/v2.2.0/flower/app.py#L71-L76", + "s0fe6c77e9c9d": "https://github.com/mher/flower/blob/v2.2.0/flower/app.py#L85-L89" + } + }, + "argo-three": { + "name": "Argo Workflows", + "basis": "v3.7.18", + "sources": { + "s262098553d2f": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/cmd/argo/commands/server.go#L191-L229", + "sbddb9f07a301": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/apiserver/argoserver.go#L263-L282", + "s2ca4f14c3164": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/gatekeeper.go#L166-L218", + "sfbb904619bfa": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/overlays/argo-server-deployment.yaml#L1-L15", + "s0ac72aa1a86c": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterole.yaml#L1-L65", + "s1689f25010be": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/cluster-workflow-template-rbac.yaml#L1-L58", + "sf0c3a52d3302": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/Makefile#L493-L515", + "sfdcdf614ee37": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/base/argo-server/argo-server-deployment.yaml#L14-L34", + "sc7ca3a37b16c": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/overlays/argo-server-deployment.yaml#L1-L4", + "s01f102470d97": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/base/argo-server/argo-server-service.yaml#L1-L11", + "s49cdaf0691c0": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/mode.go#L33-L44", + "sdb4a0c18b7c6": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/cmd/argo/commands/server.go#L113-L137", + "s36759aec55fa": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/apiserver/argoserver.go#L430-L449", + "s862033e52c9b": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-minimal.yaml#L3613-L3664", + "s73e9beeef775": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-mysql.yaml#L3678-L3729", + "s476ff15db454": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start-postgres.yaml#L3677-L3728", + "sa4b2fa875915": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterolebinding.yaml#L1-L11", + "s48db5ba07d23": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/quick-start/base/kustomization.yaml#L4-L5", + "se3ffdf2973a6": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/argo-server-rbac/argo-server-role.yaml#L1-L65", + "sb901e7f87b54": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/manifests/namespace-install/argo-server-rbac/argo-server-rolebinding.yaml#L1-L11", + "s49a8855b6da2": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/docs/argo-server-auth-mode.md#L3-L9", + "s64e74b0dbe95": "https://github.com/argoproj/argo-workflows/blob/v3.7.18/server/auth/gatekeeper.go#L106-L121" + } + }, + "argo-four": { + "name": "Argo Workflows", + "basis": "v4.1.4", + "sources": { + "s774215022573": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/cmd/argo/commands/server.go#L195-L239", + "s0f074a9ac21e": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/apiserver/argoserver.go#L299-L320", + "sb284f93cde9c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/gatekeeper.go#L189-L244", + "s0871a1dd9b13": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/docs/argo-server-auth-mode.md#L3-L9", + "sa7b9db8384be": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start/base/overlays/argo-server-deployment.yaml#L1-L15", + "s79648b46d3f5": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-telemetry.yaml#L192919-L192970", + "sd2706d04971d": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterole.yaml#L1-L67", + "sd920da4ad4b7": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/docs/quick-start.md#L3-L34", + "s144b824252c1": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/Makefile#L608-L634", + "s51645bfbc47c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/base/argo-server/argo-server-deployment.yaml#L14-L34", + "s5188f6f2efbb": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/overlays/argo-server-deployment.yaml#L1-L4", + "s639e9fe0024e": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/base/argo-server/argo-server-service.yaml#L1-L11", + "s4613c15c72af": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/mode.go#L33-L44", + "s0b16f1ac50df": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/cmd/argo/commands/server.go#L119-L143", + "sa120f74372eb": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/apiserver/argoserver.go#L476-L494", + "s6c08a5f34f2c": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-minimal.yaml#L192772-L192823", + "sa0bf6187d1db": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-mysql.yaml#L192838-L192889", + "s31f4a149576b": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start-postgres.yaml#L192837-L192888", + "s3be9b03e29be": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/cluster-install-no-crds/argo-server-rbac/argo-server-clusterolebinding.yaml#L1-L11", + "sc263bd937cf0": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/quick-start/base/kustomization.yaml#L4-L5", + "s1b134f96428f": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/argo-server-rbac/argo-server-role.yaml#L1-L67", + "s2418ee1aa49d": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/manifests/namespace-install/argo-server-rbac/argo-server-rolebinding.yaml#L1-L11", + "se758d83aaf1f": "https://github.com/argoproj/argo-workflows/blob/v4.1.4/server/auth/gatekeeper.go#L133-L148" + } + }, + "argo-docs": { + "name": "Argo Workflows documentation", + "basis": "release-3.7", + "sources": { + "s6c8e78288632": "https://argo-workflows.readthedocs.io/en/release-3.7/security/", + "s9898d4653a47": "https://argo-workflows.readthedocs.io/en/release-3.7/argo-server-sso/", + "s96145eacf345": "https://argo-workflows.readthedocs.io/en/release-3.7/rest-examples/" + } + }, + "prefect-source": { + "name": "Prefect server source", + "basis": "9e560c9b6df4e19a5109a66e66d461f9facb538d", + "sources": { + "sa4574c7ed80f": "https://github.com/PrefectHQ/prefect/blob/9e560c9b6df4e19a5109a66e66d461f9facb538d/src/prefect/server/api/server.py" + } + }, + "prefect-rolling": { + "name": "Prefect documentation (rolling)", + "basis": "unknown", + "sources": { + "sbe64d51110a2": "https://docs.prefect.io/v3/how-to-guides/self-hosted/server-cli", + "s0ef73a227667": "https://docs.prefect.io/v3/advanced/security-settings" + } + }, + "dagster-rolling": { + "name": "Dagster documentation (rolling)", + "basis": "unknown", + "sources": { + "s4c04d5da5257": "https://docs.dagster.io/guides/operate/webserver" + } + }, + "airflow-rolling": { + "name": "Apache Airflow documentation (rolling)", + "basis": "unknown", + "sources": { + "s2d1891641c91": "https://airflow.apache.org/docs/apache-airflow/stable/security/", + "s39bfd91c507b": "https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html" + } + }, + "fab-rolling": { + "name": "Apache Airflow FAB provider documentation (rolling)", + "basis": "unknown", + "sources": { + "s989f09d05161": "https://airflow.apache.org/docs/apache-airflow-providers-fab/stable/auth-manager/api-authentication.html" + } + }, + "temporal-rolling": { + "name": "Temporal documentation (rolling)", + "basis": "unknown", + "sources": { + "s12f2c64ad396": "https://docs.temporal.io/cli/command-reference/server", + "s06d4f0dbd0c8": "https://docs.temporal.io/self-hosted-guide/security", + "sa9d5614385ca": "https://docs.temporal.io/references/web-ui-configuration" + } + }, + "flower-rolling": { + "name": "Flower documentation (rolling)", + "basis": "unknown", + "sources": { + "sb1ba69f79c9b": "https://flower.readthedocs.io/en/latest/config.html" + } + }, + "tornado": { + "name": "Tornado", + "basis": "v6.5.10", + "sources": { + "sc503b4425831": "https://github.com/tornadoweb/tornado/blob/v6.5.10/tornado/netutil.py#L72-L73", + "s737c51cccd00": "https://github.com/tornadoweb/tornado/blob/v6.5.10/tornado/netutil.py#L215-L228" + } + }, + "docker": { + "name": "Moby Docker", + "basis": "docker-v29.8.1", + "sources": { + "s1f268e247a1f": "https://github.com/moby/moby/blob/docker-v29.8.1/daemon/pkg/oci/caps/defaults.go#L4-L7" + } + }, + "linux": { + "name": "Linux permissions and tools", + "basis": "unknown", + "sources": { + "s8ffd93b0efa4": "https://man7.org/linux/man-pages/man7/unix.7.html", + "sfb53d2819cd0": "https://man7.org/linux/man-pages/man7/path_resolution.7.html", + "s31f41c54750c": "https://man7.org/linux/man-pages/man5/acl.5.html", + "s3cf3d5be0f64": "https://man7.org/linux/man-pages/man7/user_namespaces.7.html", + "sa5ccbfea6e16": "https://www.kernel.org/doc/html/latest/filesystems/idmappings.html", + "s08902025e5f9": "https://man7.org/linux/man-pages/man1/getfacl.1.html", + "sa0d3793fabb7": "https://man7.org/linux/man-pages/man1/stat.1.html" + } + }, + "systemd": { + "name": "systemd", + "basis": "v257", + "sources": { + "s48ece26c16d3": "https://github.com/systemd/systemd/blob/v257/man/systemd.exec.xml#L1630-L1640" + } + }, + "fhs": { + "name": "Filesystem Hierarchy Standard", + "basis": "3.0", + "sources": { + "seafd8219f367": "https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch03s15.html" + } + }, + "iproute2": { + "name": "iproute2 ss manual", + "basis": "v6.15.0", + "sources": { + "s6cc7f285a0c7": "https://github.com/iproute2/iproute2/blob/v6.15.0/man/man8/ss.8#L358-L359", + "s9fa9a260f1de": "https://github.com/iproute2/iproute2/blob/v6.15.0/man/man8/ss.8#L7-L12" + } + }, + "network-namespaces": { + "name": "Linux network namespaces manual", + "basis": "man-pages-5.13", + "sources": { + "s427c923e346f": "https://github.com/mkerrisk/man-pages/blob/man-pages-5.13/man7/network_namespaces.7#L30-L40" + } + } + }, + "claims": { + "fronting": {"text": "Keep UI/API origins private behind HTTPS and authentication; use IdP MFA for browsers and separate machine authentication, and constrain worker egress.", "components": ["airflow", "argo-docs", "temporal-rolling"], "sources": ["airflow:s1d0b86fe4334", "temporal-rolling:s06d4f0dbd0c8", "argo-docs:s6c8e78288632"], "status": "REASONED"}, + "prefect-auth": {"text": "Self-hosted Prefect defaults unauthenticated; Basic Auth from 3.1.8 uses matching server/client auth strings and prompts the UI.", "components": ["prefect", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, + "prefect-key": {"text": "Cloud-only PREFECT_API_KEY takes precedence over PREFECT_API_AUTH_STRING and produces 401 against a self-hosted server.", "components": ["prefect", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, + "prefect-secrets": {"text": "Protect the auth string and credential-bearing Blocks; Prefect 3.1.8 block-document read routes accept include_secrets, and Secret.get() returns the underlying value. Restrict API and worker access; UI masking does not enforce authorization.", "components": ["prefect-blocks", "prefect-rolling"], "sources": ["prefect-blocks:s56081101d16a", "prefect-blocks:s1eae2c5f9f5e", "prefect-blocks:s46e69a2090c4", "prefect-rolling:s0ef73a227667"], "status": "REASONED"}, + "prefect-port": {"text": "The self-hosted Prefect server uses port 4200.", "components": ["prefect-rolling"], "sources": ["prefect-rolling:sbe64d51110a2"], "status": "REASONED"}, + "dagster-bind": {"text": "Direct dagster-webserver/dev defaults to 127.0.0.1:3000, with flag/environment overrides and free-port fallback when the port is unset or 0; explicitly bind the private origin.", "components": ["dagster"], "sources": ["dagster:sab1b42d92f27", "dagster:s6bc57f86dfe1", "dagster:s2455602a504b", "dagster:s3c7d8f17f245", "dagster:s469c458c4a71"], "status": "REASONED"}, + "dagster-helm": {"text": "The chart passes -h 0.0.0.0 on the Service port, default 80, with ClusterIP Service by default.", "components": ["dagster"], "sources": ["dagster:s695dfd19dcca", "dagster:sf6f027e96a0d", "dagster:seda3a1b24425"], "status": "REASONED"}, + "dagster-auth": {"text": "OSS webserver has no built-in login or access control; protect all routes with an authenticating proxy.", "components": ["dagster-rolling"], "sources": ["dagster-rolling:s4c04d5da5257"], "status": "REASONED"}, + "airflow-boundary": {"text": "Airflow assumes authenticated known users and is not designed for untrusted public exposure; deployment managers must keep it private.", "components": ["airflow"], "sources": ["airflow:s1d0b86fe4334"], "status": "REASONED"}, + "airflow-two-api": {"text": "Airflow 2.11.0 uses [api] auth_backends with session default; before 2.3 the name was auth_backend and 2.2.5 defaulted to deny_all.", "components": ["airflow-two", "airflow-old"], "sources": ["airflow-two:sb97f3044e215", "airflow-old:s4e72b1f51038"], "status": "REASONED"}, + "airflow-simple": {"text": "Airflow 3 defaults to development-only Simple Auth Manager, with configured users/roles and generated passwords printed to logs unless supplied.", "components": ["airflow"], "sources": ["airflow:sad02a49561a0", "airflow:s3be3e4616150"], "status": "REASONED"}, + "airflow-fab": {"text": "For production install FAB and select FabAuthManager through [core] auth_manager; verify the effective manager and use an identity backend with MFA.", "components": ["airflow", "fab-rolling"], "sources": ["airflow:s3be3e4616150", "fab-rolling:s989f09d05161"], "status": "REASONED"}, + "airflow-api": {"text": "Airflow 3 public API uses JWT independently of [fab] auth_backends, which selects FAB API backends rather than the auth manager.", "components": ["airflow", "fab-rolling"], "sources": ["airflow:s3be3e4616150", "fab-rolling:s989f09d05161", "airflow:s963b49f58a13"], "status": "REASONED"}, + "airflow-all-admins": {"text": "Keep simple_auth_manager_all_admins unset or False; enabling it disables login and grants every visitor admin.", "components": ["airflow"], "sources": ["airflow:sad02a49561a0", "airflow:s78c661193a1a"], "status": "REASONED"}, + "fab-public-role": {"text": "Leave [fab] auth_role_public and legacy AUTH_ROLE_PUBLIC unset; a configured public role can make FAB treat an anonymous visitor as logged in and supply that role's permissions.", "components": ["fab-source"], "sources": ["fab-source:s3c94f0f7176a", "fab-source:s5552a28ab9d8"], "status": "REASONED"}, + "airflow-compose-account": {"text": "Development Compose selects FAB but defaults to airflow/airflow; set credentials before account creation and update/delete an existing account separately.", "components": ["airflow-rolling", "fab-rolling", "airflow"], "sources": ["airflow-rolling:s39bfd91c507b", "fab-rolling:s989f09d05161", "airflow:s705f1eb5de00"], "status": "REASONED"}, + "airflow-jwt": {"text": "Replace Compose fallback AIRFLOW__API_AUTH__JWT_SECRET=airflow_jwt_secret and share the strong value with all signers/validators; the public fallback permits token forgery.", "components": ["airflow-rolling", "airflow"], "sources": ["airflow-rolling:s39bfd91c507b", "airflow:s705f1eb5de00"], "status": "REASONED"}, + "airflow-secret-key": {"text": "Provision the independent secret_key too: [webserver] on 2.11.0 and [api] on 3.3.2.", "components": ["airflow-two", "airflow-rolling", "airflow"], "sources": ["airflow-rolling:s2d1891641c91", "airflow-two:sabb71378ad74", "airflow:sdfcfd543928c"], "status": "REASONED"}, + "airflow-config": {"text": "Keep configuration exposure off with WEBSERVER__EXPOSE_CONFIG on 2.11.0 or API__EXPOSE_CONFIG on 3.3.2.", "components": ["airflow-two", "airflow-rolling", "airflow"], "sources": ["airflow-rolling:s2d1891641c91", "airflow-two:s6813801b7880", "airflow:s4ed64009ed8f"], "status": "REASONED"}, + "airflow-fernet": {"text": "Protect Connections, Variables, database and Fernet keys; an empty [core] FERNET_KEY selects a no-encryption implementation for new values, while configured comma-separated keys use MultiFernet. Preserve keys needed to decrypt existing values; authorized workloads still use secrets.", "components": ["airflow-crypto", "airflow-rolling"], "sources": ["airflow-crypto:sdeaf2f54c054", "airflow-crypto:s70ea7631e820", "airflow-rolling:s2d1891641c91"], "status": "REASONED"}, + "airflow-port": {"text": "Compose publishes 8080 on all interfaces; use 127.0.0.1:8080:8080 behind the fronting layer.", "components": ["airflow-rolling"], "sources": ["airflow-rolling:s39bfd91c507b"], "status": "REASONED"}, + "temporal-authorizer": {"text": "Empty authorizer selects noopAuthorizer allowing every API request, including administration; configure default authorization with trusted JWT keys and audience.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, + "temporal-claims": {"text": "Empty claimMapper selects a no-op granting system-admin claims; configure both Authorizer and ClaimMapper, with frontend TLS.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, + "temporal-health": {"text": "The built-in default authorizer permits health checks without claims; health is not an authorization discriminator.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, + "temporal-ui": {"text": "UI OIDC auth.enabled is a sibling of providers; TEMPORAL_AUTH_ENABLED plus provider settings gates the UI only, with MFA at the IdP.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:sa9d5614385ca"], "status": "REASONED"}, + "temporal-tls": {"text": "Internode and frontend mTLS are separate from UI login and API authorization.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8"], "status": "REASONED"}, + "temporal-payloads": {"text": "Keep plaintext credentials out of persisted inputs/results/history; the Temporal Go SDK v1.28.0 codec HTTP handler accepts encode/decode POSTs without an authentication check, so authenticate and restrict a deployed Codec Server separately.", "components": ["temporal-rolling", "temporal-codec"], "sources": ["temporal-rolling:s06d4f0dbd0c8", "temporal-codec:s24435f531da0"], "status": "REASONED"}, + "temporal-ports": {"text": "Frontend gRPC uses 7233 and the Web UI 8233 in the cited CLI server reference.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s12f2c64ad396"], "status": "REASONED"}, + "flower-bind": {"text": "Flower defaults to wildcard address and 5555 unless Unix socket, FLOWER_ variables or implicit working-directory flowerconfig.py override; bind loopback explicitly.", "components": ["flower", "tornado"], "sources": ["flower:s25760fae7152", "flower:s1f72a8415337", "flower:s4dd518442b7f", "flower:s4e49ebc71dc1", "tornado:sc503b4425831"], "status": "REASONED"}, + "flower-api": {"text": "Authentication defaults off; unauthenticated HTTP API is disabled unless FLOWER_UNAUTHENTICATED_API=true, which must not be mistaken for dashboard protection.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, + "flower-basic": {"text": "Basic auth uses a comma-separated credential list; supply it through FLOWER_BASIC_AUTH or protected config rather than argv and retain private TLS fronting.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, + "flower-oauth": {"text": "OAuth needs handler, client key/secret, redirect and allowed-email regex; protect the secret outside argv and prefer an MFA-enforcing provider.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED"}, + "flower-socket": {"text": "Flower passes fixed mode 0777; Tornado removes/recreates filesystem sockets on startup, so chmod does not persist. Abstract sockets have no filesystem permission boundary.", "components": ["flower", "tornado", "linux"], "sources": ["flower:s0fe6c77e9c9d", "tornado:s737c51cccd00", "linux:s8ffd93b0efa4"], "status": "REASONED"}, + "flower-directory": {"text": "Restrict socket directory search with Flower ownership, proxy-only group, mode 0750 and no outsider ACL; directory access is essential because socket mode permits writes.", "components": ["linux"], "sources": ["linux:sfb53d2819cd0", "linux:s8ffd93b0efa4", "linux:s31f41c54750c"], "status": "REASONED"}, + "flower-capabilities": {"text": "Applicable DAC capabilities bypass directory search checks; Docker defaults include CAP_DAC_OVERRIDE, while user namespaces and idmapped mounts change identity interpretation.", "components": ["linux", "docker"], "sources": ["linux:s3cf3d5be0f64", "linux:sa5ccbfea6e16", "docker:s1f268e247a1f"], "status": "REASONED"}, + "flower-runtime-dir": {"text": "Reapply directory owner/group/mode on creation: /run is cleared at boot and RuntimeDirectory defaults 0755 unless RuntimeDirectoryMode overrides it.", "components": ["systemd", "fhs"], "sources": ["systemd:s48ece26c16d3", "fhs:seafd8219f367"], "status": "REASONED"}, + "flower-numeric-ids": {"text": "A proxy sharing the socket directory must hold the required numeric group after namespace/mount mappings; names alone do not establish access.", "components": ["linux"], "sources": ["linux:s3cf3d5be0f64", "linux:sa5ccbfea6e16"], "status": "REASONED"}, + "argo-bind": {"text": "Argo Server binds wildcard :2746 with host-dependent IPv4/IPv6 support and no bind-address flag.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s262098553d2f", "argo-four:s774215022573", "argo-three:sbddb9f07a301", "argo-four:s0f074a9ac21e"], "status": "REASONED"}, + "argo-defaults": {"text": "Both tags default secure=true, auth-mode=client and hsts=true; flags and ARGO_ environment equivalents can override.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s262098553d2f", "argo-four:s774215022573"], "status": "REASONED"}, + "argo-client": {"text": "Client mode uses caller Kubernetes credentials/RBAC; v4.1.4 validates via SelfSubjectReview and requires Kubernetes 1.28+.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c", "argo-four:s0871a1dd9b13"], "status": "REASONED"}, + "argo-quickstart": {"text": "Both tags' minimal/mysql/postgres quick-starts enable server plus client mode while retaining TLS; v4.1.4 telemetry does too. Anonymous callers inherit server permissions.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sfbb904619bfa", "argo-four:sa7b9db8384be", "argo-four:s79648b46d3f5", "argo-three:s862033e52c9b", "argo-three:s73e9beeef775", "argo-three:s476ff15db454", "argo-three:s262098553d2f", "argo-three:s49cdaf0691c0", "argo-three:s2ca4f14c3164", "argo-four:s6c08a5f34f2c", "argo-four:sa0bf6187d1db", "argo-four:s31f4a149576b", "argo-four:s774215022573", "argo-four:s4613c15c72af", "argo-four:sb284f93cde9c"], "status": "REASONED"}, + "argo-quickstart-rbac": {"text": "Quick-start binds cluster-wide workflow/template and Secret get/create permissions, plus cluster-workflow-template rights; it is demo-only.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s0ac72aa1a86c", "argo-four:sd2706d04971d", "argo-three:s1689f25010be", "argo-four:sd920da4ad4b7", "argo-three:sa4b2fa875915", "argo-three:s48db5ba07d23", "argo-four:s3be9b03e29be", "argo-four:sc263bd937cf0"], "status": "REASONED"}, + "argo-installs": {"text": "Traced install inputs keep client auth/TLS with cluster-wide server RBAC; namespace-install adds --namespaced and namespace RBAC. Generated install YAML was absent.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sf0c3a52d3302", "argo-four:s144b824252c1", "argo-three:sfdcdf614ee37", "argo-four:s51645bfbc47c", "argo-three:sc7ca3a37b16c", "argo-four:s5188f6f2efbb", "argo-three:s262098553d2f", "argo-three:s0ac72aa1a86c", "argo-three:sa4b2fa875915", "argo-three:se3ffdf2973a6", "argo-three:sb901e7f87b54", "argo-four:s774215022573", "argo-four:sd2706d04971d", "argo-four:s3be9b03e29be", "argo-four:s1b134f96428f", "argo-four:s2418ee1aa49d"], "status": "REASONED"}, + "argo-service": {"text": "Base Service omits type, defaulting to ClusterIP, and maps 2746 to 2746; this does not authenticate reachable callers.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s01f102470d97", "argo-four:s639e9fe0024e"], "status": "REASONED"}, + "argo-fallback": {"text": "Remove server auth mode: adding client/SSO does not prevent anonymous fallback to server credentials; local mode uses the server kubeconfig.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s49cdaf0691c0", "argo-four:s4613c15c72af", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c", "argo-four:s0871a1dd9b13", "argo-three:s49a8855b6da2"], "status": "REASONED"}, + "argo-sso": {"text": "Enable sso.rbac.enabled and map groups to narrow ServiceAccounts through rbac-rule/precedence; without SSO RBAC, authenticated users share server permissions.", "components": ["argo-docs", "argo-three", "argo-four"], "sources": ["argo-docs:s9898d4653a47", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c"], "status": "REASONED"}, + "argo-secrets": {"text": "Store SSO OAuth credentials in referenced Kubernetes Secrets and supply API tokens through protected input, not argv.", "components": ["argo-docs"], "sources": ["argo-docs:s9898d4653a47", "argo-docs:s96145eacf345"], "status": "REASONED"}, + "argo-tls": {"text": "Keep TLS enabled with a trusted certificate Secret; no name generates a self-signed certificate, failed named-Secret loading fails startup, secure=false is plaintext and defeats HSTS.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:sdb4a0c18b7c6", "argo-four:s0b16f1ac50df", "argo-three:s36759aec55fa", "argo-four:sa120f74372eb"], "status": "REASONED"}, + "argo-metrics": {"text": "/metrics shares the server listener and auth gate unless ARGO_SERVER_METRICS_AUTH=false; server mode also admits anonymous metrics.", "components": ["argo-three", "argo-four"], "sources": ["argo-three:s36759aec55fa", "argo-four:sa120f74372eb", "argo-three:s49cdaf0691c0", "argo-three:s2ca4f14c3164", "argo-four:s4613c15c72af", "argo-four:sb284f93cde9c", "argo-three:s64e74b0dbe95", "argo-four:se758d83aaf1f"], "status": "REASONED"}, + "argo-network": {"text": "Keep 2746 private behind authenticated HTTPS; restrict direct Pod/Service access and scope server, SSO and execution-account permissions. Workflow submission permits arbitrary containers unless constrained.", "components": ["argo-docs"], "sources": ["argo-docs:s6c8e78288632"], "status": "REASONED"}, + "verify-inventory": {"text": "ss inventories only the current namespace, not publication, routing or authentication; inspect publications and external reachability separately.", "components": ["flower", "argo-three", "iproute2", "network-namespaces", "prefect-rolling"], "sources": ["prefect-rolling:sbe64d51110a2", "flower:s25760fae7152", "argo-three:sbddb9f07a301", "iproute2:s6cc7f285a0c7", "network-namespaces:s427c923e346f", "iproute2:s9fa9a260f1de"], "status": "REASONED", "verify": [1]}, + "verify-prefect": {"text": "Anonymous POST /api/flows/filter returning a JSON list is exposed; fixed is 401 with an authorized list on the same origin. Health/ready GET exemptions are version-dependent.", "components": ["prefect", "prefect-source", "prefect-rolling"], "sources": ["prefect:s12416d1a0d69", "prefect-source:sa4574c7ed80f", "prefect-rolling:s0ef73a227667"], "status": "REASONED", "verify": [1]}, + "verify-dagster": {"text": "A RepositoryConnection from /graphql is a read, including empty nodes; GraphQL/transport errors are inconclusive. Test proxy authentication and origin isolation separately.", "components": ["dagster-rolling"], "sources": ["dagster-rolling:s4c04d5da5257"], "status": "REASONED", "verify": [1]}, + "verify-airflow": {"text": "FAB 3.9.0 returning 201 with access_token for airflow/airflow is exposed; pair a valid account and anonymous/authenticated API reads at proxy and origin. Airflow 2 uses its own API/auth.", "components": ["airflow-two", "airflow-rolling", "fab-rolling"], "sources": ["fab-rolling:s989f09d05161", "airflow-rolling:s39bfd91c507b", "airflow-two:sb97f3044e215"], "status": "REASONED", "verify": [1]}, + "verify-temporal": {"text": "Credential-free workflow listing on 7233 is exposed; require rejection and a matched authorized call, not health, TLS errors or missing namespaces as proof of auth.", "components": ["temporal-rolling"], "sources": ["temporal-rolling:s06d4f0dbd0c8", "temporal-rolling:s12f2c64ad396"], "status": "REASONED", "verify": [1]}, + "verify-flower": {"text": "Anonymous UI/API should yield Basic 401 or OAuth login redirect, with a valid-session control; API-disabled is not proof the dashboard is protected.", "components": ["flower-rolling"], "sources": ["flower-rolling:sb1ba69f79c9b"], "status": "REASONED", "verify": [1]}, + "verify-socket": {"text": "Directory/socket permissions, outsider denial, reboot persistence and container numeric IDs remain reasoned; no socket or Flower process ran.", "components": ["flower", "linux"], "sources": ["flower:s0fe6c77e9c9d", "linux:s31f41c54750c", "linux:s08902025e5f9", "linux:sa0d3793fabb7"], "status": "REASONED", "verify": [1]}, + "flower-stat-run": {"text": "Tmpfs directory-only stat distinguished 0755 and 0750; the 0750 directory initially had only base ACL entries.", "components": ["linux"], "sources": ["linux:sa0d3793fabb7", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "`stat -c '%a %U:%G %u:%g %n'` printed `755` for a directory created with mode `0755` and `750` for one created with `0750`; `getfacl -p` on the `0750` directory listed only `user::rwx`, `group::r-x` and `other::---`."}, + "flower-acl-run": {"text": "A named nobody search ACL remained invisible in stat mode 750; ls and getfacl exposed the entry.", "components": ["linux"], "sources": ["linux:s31f41c54750c", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "After `setfacl -m u:nobody:--x` on the `0750` directory, `stat` still printed `750`. Only the trailing `+` in `ls -ld` and getfacl's `user:nobody:--x` line showed the named entry."}, + "flower-mask-run": {"text": "Changing the ACL mask hid named search permission; chmod 0750 restored its reported effectiveness. Access as nobody was not tested.", "components": ["linux"], "sources": ["linux:s31f41c54750c", "linux:s08902025e5f9"], "status": "DEMONSTRATED", "evidence": "After `setfacl -m m::r--`, getfacl printed `user:nobody:--x` with `#effective:---`. While masked, `stat` printed `740`."}, + "verify-argo": {"text": "Anonymous workflow JSON is exposed when server RBAC permits; missing tokens should yield 401 without server mode. A 403 alone is inconclusive; pair authorized 200 and inspect auth modes.", "components": ["argo-three", "argo-four", "argo-docs"], "sources": ["argo-three:s49cdaf0691c0", "argo-four:s4613c15c72af", "argo-docs:s96145eacf345", "argo-three:s2ca4f14c3164", "argo-four:sb284f93cde9c"], "status": "REASONED", "verify": [1]}, + "verify-origin": {"text": "Any HTTP response from the origin at an untrusted vantage proves publication; use the same responding allowed origin as control and treat connection errors/timeouts as inconclusive.", "components": ["airflow", "argo-docs"], "sources": ["airflow:s1d0b86fe4334", "argo-docs:s6c8e78288632"], "status": "REASONED", "verify": [1]} + } +} +```` +